fix(lint,metadata-protocol): a Studio publish refuses an object whose highlightFields (or any field-name list) names a missing field — the same rule family as code (#15254) - #15493
Conversation
…name a missing field
Studio's app builder mints no `view` items, so `list-view-field-unknown` had
nothing to inspect on the only artifacts the click path authors, and
`highlightFields` is an object-level list no gating rule covered:
`runtimeAuthoringRulesFor('object')` dispatched seven rules and no
reference-integrity rule among them, while the object-level existence check
that did exist (`semantic-role-field-unknown`) is warning-tier and CLI-only.
Adds `object-field-ref-unknown` (error) over the object-level field-name LISTS
nothing owns — `highlightFields` and `publicSharing.redactFields` — registers
it in the reference-integrity suite with `runtimeTypes` including `object`,
and crosses the suite entry onto the object write door so its one
object-judging member runs there. The existence half moves out of
`validateSemanticRoles`, which keeps the provenance question at that position.
Adds an `objects` plane to the ADR-0038 L3 build probes so `probes.checked`
reports how many objects were inspected — the absent key was the tell.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…r to object writes The #5378 injected-column counter-examples and the #8116 withheld-anchor case move to the rule that now owns the position, at `error` rather than `warning`. `validateSemanticRoles` keeps its own stageField copies and the provenance question, and gains the controls that keep its new silence non-vacuous. The suite member declares `runtimeTypes: ['object']` only: on a flow or view snapshot the objects are context, present in baseline and candidate alike, so every finding it could raise there cancels in the gate's differential. Adds the changeset (minor/minor, with the Migration section). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Every member of the reference-integrity suite judges flow snapshots, pinned as a roster-wide invariant rather than a per-member preference. The member declares 'flow' to hold that floor; on a flow snapshot the objects are context in both passes, so it adds a pass and no verdict. 'view' stays argued out — a view write cannot change an object's own field-name lists. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…pied harness Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…udio-highlight-fields-refusal
… limit
check:objectql-double-limit refuses a NEW limit-blind double ('the baseline
never grows'): the copied harness answered more rows than the real engine
would, so every assertion downstream of it measured a shape production never
produces. Applies the bound after the filter, by presence.
Records the new file's engine doubles in the pinned ledger, as
check:engine-double-contract prescribes for new pinned coverage.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
📓 Docs Drift CheckThis PR changes 2 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0fda3eabfb45ae6348b930eb72c8300687222d22 && git checkout 0fda3eabfb45ae6348b930eb72c8300687222d22
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6c9f34f203b89a588b237ee7d6d3ceeec6d2d3ba 9678d3418b812bc378dc5826fda33ff380526468 && git checkout -B drift-repro 6c9f34f203b89a588b237ee7d6d3ceeec6d2d3ba && git merge --no-ff 9678d3418b812bc378dc5826fda33ff380526468
node scripts/docs-audit/affected-docs.mjs --json 6c9f34f203b89a588b237ee7d6d3ceeec6d2d3ba
|
|
ACCEPT + ruling — PM seat Reviewed against the three-dot diff
Rulings on the open questions: Q1 = A (lists only:
Generated by Claude Code |
Fixes #15254
A Studio publish now refuses an object whose
highlightFields— or any object-level field-name list — names a field that does not exist on it, with the rule id and the offending path on the receipt. Error-level, on the runtime publish door as well as onos validate/os build/os lint.Measured first, on
origin/main(f01adfa5c), not on the card's installed 17.2.0 pin(a) What
objectstack validatesays today about a code-authored danglinghighlightFieldsIt is not silent — but it does not refuse, which turns out to be the same gap wearing a different face. Driving the whole
AUTHORING_RULEStable over a code-authored object withhighlightFields: ['name', 'field_10']:validateSemanticRoleswarningsemantic-role-field-unknownadvisory['cli']So
os validateexits 0 and reports the stack valid; an author who reads the verdict rather than the warning list ships it. And because the entry issurfaces: ['cli'], the runtime publish door never saw it at all. ⇒ The rule family needed the object-level clause for both surfaces, exactly as the card's fallback branch anticipated.(b) Every object-level field-name key in
packages/spec, and who owned itEach row is one mutation carrying a name that resolves to nothing, run through the whole authoring-rule table.
listViews.*.columnsis the control that proves the harness sees findings at all.highlightFields[i]object.zod.ts:2092warning/semantic-role-field-unknowncompactLayout[i](retired alias)object.zod.ts:1495warning/ samepublicSharing.redactFields[i]object.zod.ts:2258searchableFields[i]object.zod.ts:2159error/searchable-field-unknownlistViews.*.columns[i](control)object.zod.ts:2154error/list-view-field-unknownstageFieldobject.zod.ts:2107warning/semantic-role-field-unknownnameField/displayNameFieldobject.zod.ts:2075/:2082validateRecordTitle's axis — excludedindexes[].fields[i]object.zod.ts:457tenancy.tenantField/.organizationFieldobject.zod.ts:663/:671lifecycle.ttl.fieldobject.zod.ts:947created_at) — excludedactivityMilestones[].fieldobject.zod.ts:2052external.columnMap/.ignoreColumnsobject.zod.ts:1300/:1304systemFieldsobject.zod.ts:1864titleFormatobject.zod.ts:2083The rule takes the object-level lists that nothing already resolves. Which keys joined is the first open question below.
(c) The publish probe's shape, and what it can and cannot do
probes.checkedwas{ seeds, views, widgets }with noobjectskey, and probes never fail the publish they verify (protocol.ts: "probes are health surfaces; probes never fail the publish itself"). So the probe is not where a refusal can come from — it is the receipt. The refusal has to come from the #4463 runtime authoring gate, and that is where the decisive measurement was:Driving a real object publish through the gate with three dangling references —
highlightFields,searchableFieldsandlistViews.all.columns— returned exactly one unrelated error (security-owd-unset) and not one reference finding. The reference-integrity suite entry declaredruntimeTypes: ['flow', 'view'], so anobjectwrite dispatched none of it, andvalidateSemanticRolesis held off that door by the #4716 advisory-volume fence. ⇒ The only door a Studio tenant, a REST/metaauthor or an MCP/AI author has ran no reference-integrity rule at all on the artifact Studio actually authors.What changed
object-field-ref-unknown(error) —packages/lint/src/validate-object-field-refs.ts. Resolves through the sharedobject-graphseam, so the family's three skips hold: an object outside the stack, an object with no readable field map (ADR-0015external), and a registry-injected system column resolved per object (highlightFields: ['owner_id']is live on an owned object, a real miss underownership: 'none').runtimeTypes: ['flow', 'object'].objectis the point of the member;flowholds the roster-wide floor that every member judges flow snapshots (there the objects are context in both passes, so it adds a pass and no verdict).viewis argued out — a view write cannot change an object's own lists.runtimeTypesgainsobjectinauthoring-rules.ts, so the suite reaches that door; its per-member axis keeps the crossing to this one member.validateSemanticRoleskeeps provenance, drops existence at thehighlightFieldsposition — one finding per path, at one tier, instead of two tiers disagreeing about one fact.probes.checked.objects— a real plane that re-runs the judgement over each published object's ACTIVE body. Non-differential on purpose: the gate judges only what a write added ([runtime/metadata] 作者时规则只存在于 CLI:Studio/REST/MCP 的运行时授权面是第四扇门,26 条规则一条不跑——#4409 修完后最大的敞口 #4463 D4), so an object that arrived dangling before the rule existed would otherwise stay invisible forever.The click path, reproduced as a test
packages/metadata-protocol/src/protocol-publish-drafts-object-field-refs.test.tsdrives the card's steps in the natural order through the realpublishPackageDrafts(nothing on the gate path stubbed): place a field (Studio mintsfield_10) → add it tohighlightFields→ label it "Health Score" (the API name derives tohealth_score; the list is not rewritten) → publish.…and the same draft publishes cleanly once the reference is repaired. A draft save is still never gated (#4463 D1) — the author is stopped when they claim it is ready, not while they work.
File surface, and the #15395 overlap statement
Declared in the claim:
packages/lint/src/validate-list-view-field-refs.ts,packages/lint/src/authoring-rules.ts,packages/metadata-protocol/src/protocol.ts, plus tests.⭐
packages/metadata-protocol/src/protocol.tsis NOT touched by this PR — zero lines. The object plane needed nothing from it:runBuildProbesalready receives the published set and an active-body reader, andprotocol.tsrefers toBuildProbeReportbyimport type, so the shape change follows automatically. The disjointness from draft PR #15395 is therefore total rather than argued: #15395'sprotocol.tshunks span old lines 78 – 12163, and this PR's span of that file is empty. (#15395 had not merged as of theorigin/mainmerge in this branch,0db29473c.) No other file overlaps either: #15395 editsvalidate-readonly-action-writes.ts/validate-readonly-hook-writes.tsin@objectstack/lint; this PR touches neither.validate-list-view-field-refs.tsis not edited: the object-level check is a sibling rule, not a clause inside the list-view walker, so the list-view file keeps its single subject.Additions to the declared surface, each because a measurement forced it:
packages/lint/src/validate-object-field-refs{,.test}.tspackages/lint/src/reference-integrity-suite{,.test}.tsvalidateListViewFieldRefsis actually registered —authoring-rules.tsholds the suite as ONE entrypackages/lint/src/validate-semantic-roles{,.test}.tspackages/lint/src/runtime-gate.object-writes.test.tspackages/lint/src/index.tsrule-id-barrel-exports.test.tsrequires every rule id to be reachable from a published barrelpackages/metadata-protocol/src/build-probes.tsprobes.checkedand the probe planes actually livepackages/objectql/src/build-probes.test.ts,packages/runtime/src/domains/packages-publish-drafts-response-conformance.test.ts,packages/spec/src/api/protocol.test.tsprobes.checkedshapescripts/engine-double-contract.pinned.jsoncheck:engine-double-contractprescribes--writefor a new file's pinned doublesNo
content/docs/**change: thehighlightFieldsentries there are generated from the spec's own.describe()and document the key, not the old lint behaviour. Sweptdocs/qa/platform-checklist/too — the publish-lifecycle and diagnostics items it holds say "invalid drafts refused", which this strengthens; none pins the old acceptance as expected.Verification
Exit codes captured before any pipe; verdict lines quoted.
pnpm --filter @objectstack/lint testTest Files 95 passed (95),VERDICT command-exit 0pnpm --filter @objectstack/lint typecheck@objectstack/metadata-protocol— 8 publish-drafts / authoring-gate suitesTest Files 8 passed (8),VERDICT command-exit 0pnpm --filter @objectstack/metadata-protocol typechecknode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackpnpm check:declared-population-live✓ 203 of 254 famil(ies) … reach this tree's 7500 tracked file(s)pnpm check:nul-bytespnpm lint(eslint . --no-inline-config)packages/objectqlsrc/build-probes.test.tsTest Files 1 passed (1)packages/runtimesrc/domains/packages-publish-drafts-response-conformance.test.tsTest Files 1 passed (1)packages/specsrc/api/protocol.test.tsTest Files 1 passed (1)The gate family was derived after merging
origin/main(0db29473c) into this branch — the first derivation carried a⚠️ STALE TREEbanner naming 10 files the families derive from as changed, so it was discarded rather than reported. The re-derivation reports--repo 'objectstack-ai/objectstack' checked against this checkout's 'origin' remote — it holds.Re-run on the final commit
9678d3418, after the last two commits moved the tree:check:engine-double-contract,check:objectql-double-limit,check:nul-bytes,check:declared-population-live,check:cross-package-test-inputs,check:test-source-alias— all exit 0.The two exit-3s are NOT MEASURED, stated as such and not read as green:
check:dual-build-cjs-loads(PREREQUISITE NOT MET … ⛔ This is NOT a pass: nothing was measured) andcheck:type-check-debt(⛔ This is NOT a pass and NOT a finding: nothing was measured). Both require a full monorepo build closure, which CI performs.Two gates found real problems in this PR's new test file's copied harness and are now green:
check:engine-double-contract(OK — 777 pinned, 133 in the DEBT ledger, 3 exempt) after the prescribed--write, andcheck:objectql-double-limit(OK … 123 apply the caller's bound or refuse it loudly,baseline key set verified against 0db2947: no files added) after the harness'sfinddouble was made to honour the caller'slimitby presence.Declared narrowings (CI runs the full farm regardless):
@objectstack/metadata-protocolsuite (152 files) exceeds this container's 10-minute foreground cap under shared-lock contention. The 8 suites run above are its publish-drafts, authoring-gate and probe families — the ones this change can reach. CI runs the rest.check:dual-build-cjs-loads,check:type-check-debt) need a full monorepo build closure and are left to CI, reported above as NOT MEASURED rather than as passes.🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code