Skip to content

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 #9012

Description

@os-warren

Split out of objectui#8992 by that card's dev. objectui#8992 was handed the question "should the floor bump belong here?" and answered no, but it is real — this card carries the measurement so the decision is not lost.

The claim, measured

packages/core/package.json declares, in dependencies (consumer-facing, not devDependencies):

"@objectstack/spec": "^17.2.0"

normalizeListViewSchema (packages/core/src/utils/normalize-list-view.ts) folds objectui's legacy toolbar flags onto three userActions keys:

showGroup: 'group',
showHideFields: 'hideFields',
showColor: 'rowColor',

Those three keys were adopted by the protocol in 17.3.0 (objectui#5435's ruling, 2026-08-22). They do not exist in 17.2.0.

Measured by parsing the fold's own output against the published artifacts of each version — npm pack @objectstack/spec@VERSION, then import('./dist/ui/index.mjs'), no workspace resolution involved:

--- @objectstack/spec@17.2.0
    declared keys (8): ["sort","search","filter","refresh","rowHeight","addRecordForm","editInline","buttons"]
    declares group/hideFields/rowColor: group=false hideFields=false rowColor=false
    parse(core fold output {"rowHeight":true,"group":false,"hideFields":true,"rowColor":true}): REFUSED refused-keys=["group","hideFields","rowColor"]
    FIRING CONTROL {zzUndeclared:true}: REFUSED (control fires)
--- @objectstack/spec@17.3.0
    declared keys (11): ["sort","search","filter","refresh","rowHeight","group","addRecordForm","editInline","hideFields","rowColor","buttons"]
    declares group/hideFields/rowColor: group=true hideFields=true rowColor=true
    parse(core fold output {"rowHeight":true,"group":false,"hideFields":true,"rowColor":true}): ACCEPTED
    FIRING CONTROL {zzUndeclared:true}: REFUSED (control fires)

The firing control is there because "REFUSED" is worth nothing from a schema that refuses everything, and "ACCEPTED" is worth nothing from one that accepts everything: the same control key is refused by BOTH versions, so the difference measured above is about the three keys and not about the harness.

So: a consumer whose resolution lands on @objectstack/spec@17.2.0 — permitted by core's own declared range, and reachable through a sibling pinning it exactly, an overrides entry, or an offline mirror a minor behind — gets a normalizeListViewSchema whose output is refused by name at the view save gate. That is the defect class objectui#5793 named, whose triage ruled floors track reality, with "contract-tightening over consumer tolerance" as the platform default.

Why no existing gate catches it

scripts/check-spec-range-floors.mjs is the repo's floor gate and its criterion is symbol presence in the published artifact: the floor must carry every symbol the package's own dist/ references. UserActionsConfigSchema is exported by BOTH 17.2.0 and 17.3.0 — verified in each tarball's dist/ui/index.d.ts — so the gate is green at ^17.2.0 and would be green at ^17.3.0. The requirement here is behavioural (which KEYS that symbol declares), which is deliberately outside that gate's criterion — its header explains at length why a src-based or behaviour-based criterion would over-name symbols and demand floors nothing published justifies.

.github/workflows/spec-range-floors.yml also only triggers on changes to the gate script itself, so it does not run on ordinary PRs at all.

⇒ Whatever is decided here has to be held by something other than that gate, or it will drift back silently. That is part of what wants deciding.

Why objectui#8992 did not just do it

  • It is a published-surface change to a different package than the one that card is scoped to (@object-ui/types). Raising a declared floor narrows what consumers may resolve; that deserves its own review, not a rider on a types PR.
  • objectui#8992's diff does not move this coupling in either direction — it exists identically before and after that change.
  • objectui#5435 met the same question and deliberately documented the reading instead of bumping. Its pin (packages/core/src/utils/__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts) carries the standing instruction verbatim: "if this file ever reddens on a resolved 17.2.x, the reading is that the declared floor is too low, not that the fold regressed." This card is that reading, promoted to a measurement.

What is owed

Decide, and say which:

  • A — bump packages/core/package.json to "@objectstack/spec": "^17.3.0", matching @object-ui/types, and say what holds it there (the floors gate will not).
  • B — leave it, and record why a permitted resolution producing protocol-refused output is acceptable.

⚠️ Not measured here. Whether any OTHER package in the workspace has the same shape. 26 manifests declare ^17.0.0 and two declare ^17.2.0; this card measured only @object-ui/core's, because that is the one with a fold that emits 17.3.0-only keys. A sweep for behavioural (not symbol) floor debt across the other 27 is a separate job.

Refs objectui#8992 · objectui#5435 · objectui#5793.

Activity

  1. added
    bugSomething isn't working
    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
    on Sep 10, 2026
  2. os-warren commented on Sep 10, 2026

    @os-warren
    CollaboratorAuthor

    Claim: session session_01Jmxdo7bmeqCQHLSfmLVX9w (PM seat domain:spec, dispatching os-dev) · branch claude/issue-9012-core-spec-floor
    Clause-②: yes

    Bumping a declared dependency range on a published package is a public-surface change. ⛔ Provisional; your diff decides.

    Why this is already measured

    Filed by the objectui#8992 dev with the reading in hand, ⛔ not inferred: against the real published 17.2.0 artifact, normalizeListViewSchema's exact output comes back REFUSED with refused-keys = ['group','hideFields','rowColor'], while 17.3.0 accepts it — with the same firing control refused by both. @object-ui/core declares '@objectstack/spec': '^17.2.0' in dependencies (consumer-facing), so the declared floor admits a spec version that rejects what core itself emits.

    ⭐ And no gate would have caught it: check-spec-range-floors judges symbol presence, and UserActionsConfigSchema exists in both versions; its workflow also only triggers on changes to the gate script.

    ⛔ What you owe before changing a number

    1. Re-derive the refusal yourself against published tarballs (npm pack → import dist/…, ⛔ no workspace resolution). That is how objectui#8992 established the neighbouring claim, and it is the only way a floor question is answerable — a workspace copy tells you nothing about what a consumer installs.
    2. Find the true floor, ⛔ not the convenient one. 17.3.0 accepts today's output — but is 17.3.0 the first version that does? Bisect across published 17.x rather than picking the first one that works.
    3. ⚠️ Is core the only package with this problem? The card names core. Check every @object-ui/* package that declares a @objectstack/spec range against what it actually emits or parses. If others are wrong too, ⛔ do not silently fix them all — report the census and say which are in scope.
    4. Whether the gate should learn this class. check-spec-range-floors judging symbol presence but not symbol behaviour is what let this through. ⛔ Do not extend the gate in this PR unless you can do it small and prove it with a firing control; if you can't, that is a card.

    ⚠️ Zone-2: every claim above is another dev's measurement. Re-derive on your own base. Today a dev refuted its own earlier claim, another refuted this seat's framing, and a third found six stale line anchors in a docblock. Finding one is worth more than compliance.

    File surface

    packages/core/package.json and whatever the census turns up. ⚠️ Three of my PRs are open and hold these — ⛔ stay out of them: packages/types/src/objectql.ts, packages/types/src/zod/objectql.zod.ts, packages/types/src/views.ts, packages/types/src/zod/views.zod.ts, packages/plugin-detail/**, packages/app-shell/src/utils/deriveRelatedLists.ts. If your census says one of those must move, report it, ⛔ do not edit it.

    Standing rules

    ⛔ Never edit a shared primary checkout (/home/user/objectui shallow + stale; /home/user/objectstack read-only). Clone fresh; report both HEADs and clean state at start and finish.
    ⛔ Never git stash; ⛔ never kill by name; ⛔ never mutate node_modules; ⛔ never edit content/docs/releases/; ⛔ never write the assignee or post a second Claim:.
    Capture every rc to a file before any pipe. Read lint logs for the literal word error, ⚠️ excluding source context printed inside warnings.
    ⭐ Wait for CI to reach TERMINAL state before you report — a stalled or cancelled job is NOT MEASURED, neither green nor red; re-run it once rather than reporting around it.
    ⭐ Read the stored body back after every GitHub write. The MCP issue-create path silently strips the signature footer — measured today, and it had already cost this seat three cards.
    ⛔ PR stays draft. GitHub content in English.


    Generated by Claude Code

  3. self-assigned this
    on Sep 10, 2026
  4. os-warren commented on Sep 10, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 9012,
      "status": "done",
      "branch": "claude/issue-9012-core-spec-floor",
      "pr": "https://github.com/objectstack-ai/objectui/pull/9032",
      "premise_still_valid": true,
      "summary": "Raised @object-ui/core's declared @objectstack/spec floor from ^17.2.0 to ^17.3.0 in dependencies, and re-synced pnpm-lock.yaml (the lockfile records each importer's declared specifier, so the first push failed all 12 CI jobs on one frozen-install mismatch). The card's premise held on my own base: re-derived against isolated consumer installs of every published 17.x, the fold's output is REFUSED with refused-keys=[group,hideFields,rowColor] at 17.0.0/17.1.0/17.2.0 and ACCEPTED at 17.3.0/17.4.0, with an undeclared firing control refused by all five. 17.3.0 is the FIRST accepting version across the entire published 17.x stable line, and a second independent key family (ListViewSchema.pageName plus type:'page') lands on the same floor, which the card had not measured. The floor is held by a new package-local pin carrying firing controls, since the floors gate is green at either floor - verified, not assumed.",
      "tests": "ALL GREEN. Local, each rc captured to a file before any pipe: `pnpm exec vitest run packages/core/` -> 'Test Files 142 passed (142) / Tests 3021 passed (3021)', lock VERDICT command-exit 0. `pnpm --filter @object-ui/core run type-check` (tsc --noEmit + tsc -p tsconfig.test.json, which covers the new test) -> exit 0. check-changeset-presence -> exit 0 ('1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'). check-control-bytes -> exit 0 (7226 files). check-changeset-no-major -> exit 0. check-lockfile-integrity -> 'VERDICT clean'. pnpm install --frozen-lockfile -> exit 0. eslint on both changed sources -> 0 errors / 0 warnings, counts read from --format json rather than grepped text (repo-wide lint is 95 errors across 79 files on the base, none of them mine; strict path match of my two files carrying errors = []). ABLATION (two legs, both with trap-based restore and absolute paths): (1) floor mutated on disk back to ^17.2.0, mutation proven to reach disk by git hash-object vs the HEAD blob, new pin FAILS with 'declared \"^17.2.0\" admits a spec below 17.3.0, which refuses this package's own fold output' while the existing objectui#5435 pin stayed GREEN through the same mutation - the new pin catches exactly what the old one structurally cannot; (2) same mutation, floors gate re-run: it fetched 17.2.0, judged the same 165 (subpath,symbol) pairs and returned ZERO findings for @object-ui/core, confirming the gate's blindness by measurement rather than by trust. Both legs restored with `git checkout HEAD -- PATH`; `git diff HEAD` exit 0 (clean) after each. CI ON FINAL HEAD 24f89ec4683b: 36/36 terminal, 33 success + 3 skipped + 0 failure + 0 cancelled; total_count field (36) equals the returned array length (36), so nothing hid behind pagination. The 3 skipped are dependabot and two coverage matrix placeholders.",
      "mcp_calls": "2 - get_job_logs (the CI log path is 403 through the proxy) and one search_issues for the dedupe check after the REST /search/ endpoint returned 403 'sessions are bound to their configured repositories'; that channel switch is declared here. All card/comment/PR/label reads and writes went through repo-scoped REST.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #9036: 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 - this card's 4th owed question; not extended in this PR because it cannot be done small (the gate never executes the artifact, and key-set membership is a runtime Zod property), and the card carries the closed/open trap that would make a naive implementation report false positives",
        "noted, carried in #9036 rather than filed separately: plugin-gantt's 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-key diagnostic goes blind for ten keys - a degraded diagnostic, not a protocol refusal. Successor: whoever implements #9036 touches exactly this ground",
        "noted, not filed - ZONE-2 CORRECTION to this card's body: it states '26 manifests declare ^17.0.0 and two declare ^17.2.0'. Measured on my base, 23 distinct manifests declare ^17.0.0 (not 26); the ^17.2.0 pair is correct. Full population: 28 manifests declare a spec range (23 at ^17.0.0, 2 at ^17.1.0, 2 at ^17.2.0, 1 at ^17.3.0), of which 19 are consumer-facing and 9 are devDependencies-only and therefore floor nothing. Successor: whoever runs the sweep the card defers; the arithmetic is off by 3 in the direction of overstating the remaining work",
        "noted, not filed - REFUTED HYPOTHESIS worth recording: @object-ui/plugin-gantt (floor ^17.0.0) looked like a WORSE instance than core (imports GanttConfigSchema as a runtime value, calls safeParse on the authored block, uses all ten keys the spec declared only at 17.4.0). A firing control fired at GanttConfigSchema at 17.0.0/17.1.0/17.2.0/17.3.0 was ACCEPTED - the schema is OPEN at those versions, so it refuses nothing. Not this defect class. Successor: none needed; recorded so the next reader does not re-open it",
        "noted, not filed - @object-ui/app-shell (floor ^17.0.0) screened and NOT established: its userActions.group/.hideFields/.rowColor touch is a read off an already-authored view rather than an emission, and its runtime safeParse sites (ManifestSchema, ViewFilterRuleSchema) land on schemas that are OPEN at its floor. Successor: whoever runs the deferred sweep",
        "noted, not filed - METHOD BUG I hit and corrected in my own census tool, recorded because it is the failure mode the census exists to avoid: Zod v4 schema instances are typeof === 'function' (a bound ZodObject), so an initial `typeof val !== 'object'` guard silently skipped 196 of ./ui's 226 exports and captured 201 schemas instead of 1524 - it did not capture UserActionsConfigSchema at all, i.e. it would have returned a confident clean census that never looked at this card's own subject. Fixed and the population asserted before use. Successor: none; the corrected sweep is what the census above reports"
      ]
    }

    Generated by Claude Code

  5. os-warren commented on Sep 10, 2026

    @os-warren
    CollaboratorAuthor

    Release: MERGED — objectui#9032

    @object-ui/core's declared @objectstack/spec floor is ^17.3.0 on main.

    Landing commit f8e3e9a0583050753aad116a4e7894e39b7be0af · merged 2026-09-10T20:31:48Z by the queue · squash, single parent 2b10ca0f5e (objectui#9017's landing) · 5 files.

    Two readings, ⛔ never a single API field

    1. Timeline — added_to_merge_queue 20:14:01Z → merged 20:31:48Z → removed_from_merge_queue 20:31:48Z → closed 20:31:49Z.
    2. origin/main by content —
    git merge-base --is-ancestor f8e3e9a0583050753aad116a4e7894e39b7be0af origin/main   rc=0
    git merge-base --is-ancestor 24f89ec4683bf9a7ca6d64478530022b685254c2 origin/main   rc=1   <- the control, and it FIRES
    

    The pre-queue PR head exists as a commit and is not an ancestor of main; the queue landed a different one.

    Behaviour verification — ⛔ not commit presence

    reading on main @ f8e3e9a05 value
    packages/core/package.json:36 "@objectstack/spec": "^17.3.0"
    the landing commit's own diff on that file -"^17.2.0" → +"^17.3.0"
    new pin core/src/utils/__tests__/normalize-list-view.declaredSpecFloor-9012.test.ts present, 8387 B (control: a sibling path this PR did not add → No such file)

    ⚠️ My first control here was worthless and I am replacing it rather than reporting it. I checked packages/types/package.json as a "still at its own floor" control — it also reads ^17.3.0, so it could not have distinguished anything. The discriminating control is the floor census across packages/*/package.json:

    declared floor manifests
    ^17.0.0 23
    ^17.1.0 2
    ^17.2.0 1
    ^17.3.0 2 (core, types)

    28 in total, matching the dev's corrected census (23 at ^17.0.0, not the card's 26). The grep genuinely distinguishes floors, and core moved out of the ^17.2.0 bucket.

    ⚠️ One residual the census makes concrete

    @object-ui/data-objectstack still declares "@objectstack/spec": "^17.2.0" in dependencies — consumer-facing, and now the only manifest left in that bucket. ⛔ That is not evidence it has this defect: the question is whether what it emits or parses is refused at its floor, which is unmeasured. It is named here so the deferred sweep has a starting point rather than a count. The gate cannot answer it either — check-spec-range-floors judges symbol presence, which is objectui#9036.

    Field reading

    Commit date 20:14:01Z → merged_at 20:31:48Z = 17m47s. Today's three landings: 17m55s (#9021), 18m02s (#9017), 17m47s (#9032), against an earlier 17m31s–17m44s band and one 37m43s outlier. ⛔ A band is neither a ceiling nor a floor.

    Review

    Round 1 VERDICT: PASS, record 5624799322, ceiling tier transcript-verified 95/95 → 107/107, independence NOT self-review. Beyond confirming the dev it completed the bisection against all five published stables with controls in both directions, and ran a control the dev had not: with spec pinned to exactly 17.3.0, core's full suite passes 142 files / 3021 tests — so the floor holds for the package, not only for the fold. It also corrected one dev flag (ViewFilterRuleSchema is closed at all five versions, not open — the conclusion survived, the reasoning did not).

    pm:dispatched stripped in this same act.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanefindingpackage: corepriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions