Skip to content

[P0] Studio package publish accepts a dangling highlightFields reference — the authoring gate never sees what the app builder produces #15254

Closed

Description

@yinlianghui

Finding from the promo-video lane (steedos-labs/video-studio#5, "Studio modeling — clicked changes pass the same gate as code"). Filed as a finding only; not claiming it. Blocks promo video https://github.com/steedos-labs/video-studio/issues/5 — the film's one claim is "a change made by clicking in Studio is refused by the same gate that refuses code", and on the pinned stack there is nothing Studio can produce that the gate refuses.

Measured (hotcrm main @ 5e08628, @objectstack/* 17.2.0, console Studio; dev server, then snapshot-restored — zero residue)

  1. In a writable package (New package (writable base)), click-create an object, then a Number field. The field starts as API name field_10 / label New field.
  2. In the object's Settings, add that field to Highlight fields (highlightFields) — default list columns… via + Add field… → the list now references field_10.
  3. Back on the Form, set the label to Health Score → the API name auto-derives to health_score. highlightFields still says field_10. A dangling reference, produced purely by clicking in the natural order (name the field after placing it).
  4. Publish: POST /api/v1/packages/<pkg>/publish-drafts → HTTP 200, outcome:"published", failedCount:0, probes:{"issues":[],"checked":{"seeds":0,"views":0,"widgets":0}}. UI shows Published all drafts in this package (one atomic release).
  5. After publish highlightFields is still ["field_10"]; the app's list renders without any error — the bad reference is silently ignored at runtime.

Why the gate stays quiet

list-view-field-unknown (error-level, in AUTHORING_RULES) checks view.columns. The Studio app builder does not mint view items at all — GET /api/v1/meta/view lists 89 views, 0 belonging to the new package — and highlightFields is an object-level property that probes.checked does not cover (views:0). So the reference-integrity rule has nothing to inspect on the only artifacts Studio authors. The same stack's objectstack validate would presumably flag a code-authored object with an unknown highlightFields entry, which is exactly the asymmetry the video was supposed to deny.

Minimal capability the film needs (no wish list)

Publishing a Studio draft whose highlightFields (or any field-name list on an object) names a field that does not exist on that object must fail the publish with a rule id and the offending path on screen — the same rule family that objectstack validate applies to code-authored objects. Warning-level is not enough for the claim; it has to refuse.

Also observed, for context (by design, not the ask)

Code-loaded package objects are read-only in Studio (object.supportsOverlay=false, PUT /api/v1/meta/object/crm_opportunity → 403 NOT_OVERRIDABLE for crm_opportunity / crm_campaign / crm_case), per ADR-0005/0010. The video's original scene ("add a field to Opportunities") is therefore not filmable on hotcrm; the writable-package route above was the fallback, and it is where this finding came from.

Related: objectstack-ai/objectui#2260 (Studio field API-name derivation) and the follow-up filed alongside this one in objectui.

Activity

  1. added
    priority:p0Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serial
    on Sep 4, 2026
  2. added theissue type on Sep 4, 2026
  3. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    domain:devxDomain lane — seat card indexed by label:pm:seat
    on Sep 4, 2026
  4. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    Triage: cross-lane — domain:devx designated as the claiming lane, one PR. pm:queue · Bug · priority:p0 retained.

    Triage seat, session session_01SwJQDFKe8tVit3BXQ9EfR5, R+145, 2026-09-04T18:50Z.

    ⚠️ This card sat 11 hours carrying priority:p0 and nothing else — no domain:*, no pm:* state, no assignee. ⇒ It was invisible to every lane's candidate query and unclaimable under the lane table, because domain:* has one producer. A p0 that no seat can see is a p0 that nobody is working on. Recorded as a process failure, not as a criticism of the filer, who correctly left routing to triage.

    Landing points, read on origin/main at 18:48Z

    half file lane
    the rule family (list-view-field-unknown) packages/lint/src/validate-list-view-field-refs.ts domain:devx
    the roster it is wired through packages/lint/src/authoring-rules.ts domain:devx
    the publish probe (probes.checked) packages/metadata-protocol/src/protocol.ts domain:engine

    domain:devx claims it. The substantive half is defining what "a field-name list on an object" is and which keys belong to it — that is lint's, and the family already lives there. The metadata-protocol limb is wiring the probe to run it over objects, which probes.checked does not cover today ({seeds:0, views:0, widgets:0} — no objects key at all).

    Per the cross-domain path: declare the full file surface across both lanes in the claim, and run the directed in-flight check against domain:engine's claims before claiming.

    Why the gate is silent — the card's diagnosis, and it is the whole fix

    list-view-field-unknown inspects view.columns. The Studio app builder mints no view items at all — GET /api/v1/meta/view returned 89 views, 0 belonging to the new package — and highlightFields is an object-level property the probes do not cover.

    ⇒ The reference-integrity rule has nothing to inspect on the only artifacts Studio authors. ⛔ Not a rule that missed a case; a rule pointed at a surface the click-path never produces.

    The acceptance bar, quoted because "warning" will not do

    Publishing a Studio draft whose highlightFields (or any field-name list on an object) names a field that does not exist on that object must fail the publish with a rule id and the offending path on screen … Warning-level is not enough for the claim; it has to refuse.

    ⭐ And the reproduction is a click-path in the natural order, not a contrived one: place the field, then name it. The API name auto-derives from the label (field_10 → health_score) while highlightFields keeps the old id. ⇒ Any author who names a field after placing it produces this.

    priority:p0 retained, with the reasoning made explicit

    The filer named a concrete blocked deliverable (the promo video's single claim: "a change made by clicking in Studio is refused by the same gate that refuses code"), and on the pinned stack there is nothing Studio can produce that the gate refuses. ⛔ Triage does not downgrade a p0 filed against a named external blocker on its own reading.

    ⚠️ Worth stating separately for the maintainer, because it outlives the video: the asymmetry itself is the defect. objectstack validate would flag a code-authored object with an unknown highlightFields entry; the click path publishes it green and the app then silently ignores the bad reference at runtime. That is declared-not-enforced on the authoring surface AI and humans share.

    ⛔ Out of scope, and by design: code-loaded package objects are read-only in Studio (supportsOverlay=false, 403 NOT_OVERRIDABLE) per ADR-0005/0010. ⛔ Do not treat that as part of this card.

    ⚠️ Measured on hotcrm main @ 5e08628 with @objectstack/* 17.2.0 — an installed pin, not this tree. Re-verify the probe shape and the rule's scan surface on origin/main before implementing.


    Generated by Claude Code

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 4, 2026
  6. claude commented on Sep 4, 2026

    @claude
    Contributor

    Claim: PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk, dispatching to an os-dev subagent on branch claude/issue-15254-studio-highlight-fields-refusal. P0, cross-lane with domain:devx designated (triage 15:30Z): file surface declared across both lanes — packages/lint/src/validate-list-view-field-refs.ts, packages/lint/src/authoring-rules.ts (devx) and packages/metadata-protocol/src/protocol.ts (engine, the publish probe). Directed in-flight check against domain:engine: open draft PR #15395 (#14147, needs:contract-review, human-gated) edits protocol.ts +65/−275 — this PR keeps its protocol.ts hunk minimal and additive (the objects probe wiring only) and merges main if #15395 lands first; no other open PR touches the three files. Main at f01adfa5c. Assignee field was empty at the read immediately before this write.


    Generated by Claude Code

  7. claude commented on Sep 4, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 15254,
      "status": "done",
      "branch": "claude/issue-15254-studio-highlight-fields-refusal",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15493",
      "premise_still_valid": true,
      "summary": "The card's defect reproduces and is fixed: a Studio object publish carrying a dangling highlightFields reference now FAILS with rule id object-field-ref-unknown and path objects.PKGOBJ.highlightFields[i]. One correction to the card's guess, measured on origin/main: objectstack validate is NOT silent on a code-authored dangling highlightFields — it reports semantic-role-field-unknown, but at WARNING, advisory tier, surfaces ['cli'], so the command exits 0 and the runtime publish door never saw it. The decisive measurement widened the diagnosis: runtimeAuthoringRulesFor('object') dispatched seven rules and NO reference-integrity rule at all, so the only door a Studio/REST/MCP author has ran no reference-integrity judgement on the artifact Studio actually authors. Fix: new error-tier rule validateObjectFieldRefs over the object-level field-name LISTS nothing owned (highlightFields + publicSharing.redactFields), registered in the reference-integrity suite with runtimeTypes ['flow','object'], the suite entry crossed onto the object door, the existence half moved out of validateSemanticRoles (which keeps the provenance question), and an objects plane added to the ADR-0038 L3 probes so probes.checked reports objects: N. protocol.ts needed ZERO lines — the probe machinery lives in build-probes.ts — so the hunk is trivially disjoint from draft PR #15395 (whose protocol.ts hunks span old lines 78-12163). Assignee was baozhoutao at my read, not empty as the claim recorded; I did not touch it.",
      "tests": "pnpm --filter @objectstack/lint test -> 'Test Files 95 passed (95)', lock VERDICT 'command-exit 0'; lint typecheck clean same run. @objectstack/metadata-protocol: 8 publish-drafts/authoring-gate suites -> 'Test Files 8 passed (8)', VERDICT 'command-exit 0'; mp typecheck clean same run. Cross-package pins each 'Test Files 1 passed (1)': objectql/src/build-probes.test.ts, runtime/src/domains/packages-publish-drafts-response-conformance.test.ts, spec/src/api/protocol.test.ts. Whole-repo pnpm lint (eslint . --no-inline-config) exit 0 — full farm, NOT a narrowed run. Derived family (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived AFTER merging origin/main 0db29473c because the first derivation carried a STALE TREE banner naming 10 family-source files as changed): 69 commands, 67 exit 0, 2 exit 3. The two exit-3s are NOT MEASURED and are reported as such, never as green: check:dual-build-cjs-loads ('PREREQUISITE NOT MET ... this is NOT a pass: nothing was measured') and check:type-check-debt ('NOT a pass and NOT a finding: nothing was measured') — both need a full monorepo build closure, which CI performs. check:declared-population-live exit 0 ('203 of 254 families ... reach this tree 7500 tracked files'); check:nul-bytes exit 0, plus a manual control-character scan over the whole diff (grep -naP over every added/modified file) with zero hits. TWO gates found REAL problems in this PR's new test file and are now green: check:engine-double-contract (new file's engine doubles unrecorded -> ran the prescribed --write; now 'OK — 777 pinned, 133 in the DEBT ledger, 3 exempt') and check:objectql-double-limit (the copied harness's find double was limit-blind and 'the baseline never grows' -> the double now applies the caller's bound after the filter by presence; now 'OK ... 123 apply the caller bound or refuse it loudly', 'baseline key set verified against 0db2947: no files added'). RATCHET RE-RUN on the final commit 9678d3418 after the last commits moved the tree: engine-double-contract, objectql-double-limit, nul-bytes, declared-population-live, cross-package-test-inputs, test-source-alias — all exit 0. Every exit code captured with 'cmd > file 2>&1; EXIT=$?' before any pipe; no ablation was run, so none is claimed. ONE DECLARED NARROWING: the full @objectstack/metadata-protocol suite (152 files) exceeds this container's 10-minute foreground cap under shared-lock contention; the 8 suites run are its publish-drafts, authoring-gate and probe families.",
      "mcp_calls": "0 — every GitHub read (card, comments, PR 15395 files, open-issue dedup over 270 issues) and every write (PR create, 2 findings, this comment) went through the repo-scoped REST channel, probed 200 at the start of the run. No MCP GitHub tool was invoked.",
      "open_questions": [
        {
          "question": "Which object keys joined the new rule? I took the LISTS that no rule already owns — highlightFields (+ its retired compactLayout spelling, read only so the raw lint path keeps the coverage it inherited) and publicSharing.redactFields — and left every SCALAR pointer out. Is redactFields in scope, and should the scalars follow?",
          "options": [
            "A (shipped) — lists only: highlightFields + publicSharing.redactFields. redactFields is the card wording (any field-name list on an object) applied literally, it is owned by nothing, and it fails OPEN: the schema history note says a silently dropped key there serves a field the author meant to redact to whoever holds the share link. Cost: a tenant with a stale redactFields entry is now refused on republish.",
            "B — highlightFields alone, the literal card. Smallest refusal surface; leaves a security-relevant dangling reference reported by nothing, and the rule reads as one-key rather than as a charter.",
            "C — A plus the scalar role pointers (stageField, nameField/displayNameField). Consistent (stageField and highlightFields are one ADR-0085 sentence apart) but promotes two more warnings to refusals in the same PR, and nameField is validateRecordTitle axis, so it needs that owner adjudicated first."
          ],
          "recommendation": "A, because it is the card bar read literally, both keys are owned by nothing, and redactFields is the only one of the candidates where the silent failure is a disclosure rather than a rendering miss. C is a real follow-up but each scalar deserves its own blast-radius measurement — stageField in particular is the one key where the platform documents a graceful fallback (heuristic stage detection), which is the argument AGAINST refusing that the list positions do not have."
        },
        {
          "question": "The suite entry now dispatches on object writes, which is a REFUSAL widening on the hottest write door — unlike #4716, which was fenced by the advisory tier. An object republished carrying a pre-existing dangling highlightFields is now 422 rather than warned. Ship as-is, or stage it?",
          "options": [
            "A (shipped) — ship the refusal. It is the card acceptance bar in its own words (Warning-level is not enough for the claim; it has to refuse), the gate differential means a STORED sibling in violation never blocks someone else write (#4463 D4), and the changeset Migration section names the rule id and both fixes.",
            "B — land the rule at error on the CLI but hold it off the runtime door for one release, so tenants sweep first. Halves the value: the click author is exactly the one the card is about, and the CLI is the surface they do not have."
          ],
          "recommendation": "A. B leaves the film claim false for another release and it is the one surface the card was filed about. The migration risk is real but bounded and self-describing — the refusal names the path and the fields the object actually has."
        }
      ],
      "out_of_scope_findings": [
        "filed as #15494: indexObjectGraph throws a TypeError on a null entry in stack.objects, so every reference-integrity rule crashes on it — the shared seam is the outlier, one guard there closes the class (deliberately not patched inside one member, which would hide it in the siblings)",
        "filed as #15495: the object write door still runs no field-existence rule for objects[].searchableFields or objects[].listViews.* — both are ERROR-tier on the CLI but their suite members declare runtimeTypes [flow, view], so the same click-author asymmetry #15254 closed for highlightFields remains open one key over; widening wants its own false-positive measurement over a real object corpus"
      ]
    }

    Generated by Claude Code

  8. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 4, 2026
  9. claude commented on Sep 4, 2026

    @claude
    Contributor

    LANDED — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

    PR #15493 merged 2026-09-04T19:10:37Z (merge-queue; merged: true, main 615fac3a0). Probe on the re-fetched main: packages/lint/src/validate-object-field-refs.ts + its test and the changeset are present; OBJECT_FIELD_REF_UNKNOWN exported; the reference-integrity suite entry carries runtimeTypes ['flow','view','object']; the validateObjectFieldRefs member is registered in authoring-rules.ts; build-probes.ts counts objects. The P0 Fixes closed this card on merge; pm:dispatched and the assignee are stripped. Rulings recorded on the PR (Q1 = A lists-only, Q2 = A ship the refusal). Follow-ups already graded on the reference-integrity suite: #15494 (p2) → #15495 (p3), dispatchable on the next slot.


    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

No one assigned

    Labels

    bugSomething isn't workingdomain:devxDomain lane — seat card indexed by label:pm:seatpriority:p0Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serial

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions