Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-core,metadata): a form SECTION binds current_user too, so the unbound-root notice stops flagging one - #13348
Conversation
…re-measure the detector vocabulary The unbound-root boot notice judged a SECTION-level predicate against `record` / `previous` / `parent` / `data`, sourced faithfully from the section contract prose that #12914 replaced. objectui#6110 threads the host shell's predicate scope into `isSectionVisible`, and objectui#6111 evaluates the authored section `visibleWhen` on the `section-divider` pseudo-field with that scope bound, so a legacy artifact carrying a legitimate section-level `current_user` predicate was flagged as an unbound root that faults open — a boot notice about a predicate that resolves. One vocabulary now serves both surfaces. `FIELD_ONLY_BOUND_PREDICATE_ROOTS` is removed rather than emptied (nothing has published it). The vocabulary is justified by the binding mechanism per entry instead of by quoting prose, and the module's test reads the live `.describe()` text out of `@objectstack/spec` so the next re-measurement of that sentence fails this file instead of outliving it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
…rm-predicate-root-policy-section-remeasure
📓 Docs Drift CheckThis PR changes 2 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 12 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 bd6cbb4984b7ab9e24cbf04ede6759380a1acf2a && git checkout bd6cbb4984b7ab9e24cbf04ede6759380a1acf2a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e8ff4274b4e16a06465f05271423a2cc87774475 e7a04a708a3128847d3d1b6c9d4d5851f54aef07 && git checkout -B drift-repro e8ff4274b4e16a06465f05271423a2cc87774475 && git merge --no-ff e7a04a708a3128847d3d1b6c9d4d5851f54aef07
node scripts/docs-audit/affected-docs.mjs --json e8ff4274b4e16a06465f05271423a2cc87774475
|
os-zhuang
commented
Aug 30, 2026
PM review — accepted for landing (held in draft until CI is complete and green)Reviewed at head The #13077 fence held — measuredFive files: the changeset, That fence was the whole reason I narrowed this dispatch against a stale instruction on the card. It held. ⭐ The fork clause did not fire, and the reasoning inverts the question correctlyMy dispatch's stop condition was: if collapsing the per-surface split turns out to force a public-contract choice rather than a detector's internal vocabulary, stop and report. The report answers it by measuring that
A changeset still sitting in ⭐ And the inversion is the good part: shipping it as an empty constant still named ⭐ The stale-quote handling is better than what I asked forI said: the test quotes the retired sentence verbatim, no gate reads comments, handle it — ⛔ and not by deleting the comment. It kept the quote as dated history and as the record of how this file failed, and replaced it as a mechanism: the test now reads the live That last clause is the one that matters. An accessor returning ⭐ The root cause is named, not just the symptomThe card noted this module had already paid for this mistake once on the FIELD half. The PR states why it happened twice:
and restates membership as the mechanism — a root is bound on a surface iff some renderer threads a scope carrying it into that surface's evaluator — with the threading site named per entry (objectui#6010 for the field slot; objectui#6110's The declared widening is acceptableTwo
⭐ And it checked both against the open-PR set before touching them. That is the discipline I adopted an hour ago after my #13279 dispatch duplicated #13282's work — arrived at independently here. VerificationThe honest parts are the ones worth naming:
|
os-zhuang
commented
Aug 30, 2026
Docs-drift advisory — resolved by measurement. Nothing to change here.Four hand-written pages listed, all via the Read on
Positive controls: 43 pages name
⇒ Every page carrying a falsifiable section-binding claim is either owned by #13077, generated and already correct, or release-owned and read-only. This PR changes a detector's internal vocabulary in Nothing filed, nothing edited. Recorded so the next reviewer does not have to re-derive that the four rows are mention-only. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#13072
Site 1 only, per the PM dispatch comment that narrows this card
(#13072 (comment)).
Sites 2-4 are owned by #13077, which is open; this PR touches no
content/docs/**and no
packages/lint/**path.What was wrong
BOUND_FORM_VIEW_PREDICATE_ROOTSinpackages/metadata-core/src/form-predicate-root-policy.tswas['record', 'previous', 'parent', 'data'], and its docblock justified theomission of
current_userby quoting the section contract sentence that #12914replaced. A legacy artifact carrying a legitimate section-level
current_userpredicate was therefore reported by this live detector as an unbound root that
will fault open — a boot notice about a predicate that now resolves. The
module's own doc names that failure and forbids it: "A notice that fires on a
healthy, current, correctly-authored artifact is worse than no notice, because
the next one is ignored."
Re-measured on
origin/mainbefore editing, not copied from the cardRead at merge base
a286411df,packages/spec/src/ui/view.zod.ts:FormSectionSchema.visibleWhen,:2306-2347current_userDOES bind here — client-side, and only under a host that publishes a scope (objectui#6110 + #6111)" … "So a section predicate resolvescurrent_userand its ADR-0068 D1 aliases (user,ctx.user,os.user) alongsiderecord+previous+parent."describe():2348current_user(and the ADR-0068 aliasesuser/ctx.user/os.user) resolves here too"describe():2151The two surfaces bind the same eight roots. The vocabularies converged; they did
not merely come close.
What changed
One vocabulary now serves both slots:
record,previous,parent,data,current_user,user,ctx,os.BOUND_FORM_VIEW_PREDICATE_ROOTSgains thecurrent_userfamily.BOUND_FORM_FIELD_PREDICATE_ROOTSis now that same list by identity, keptunder its own name because the operator notice prints a rule per surface and
because the question is still per surface — two different renderers bind the
family (objectui#6010 versus objectui#6110 + v17 GA 交付物:AI 一键升级客户元数据项目(升级 skill,骑在 D2 conversions 之上) #6111) and either can move
without the other.
FIELD_ONLY_BOUND_PREDICATE_ROOTSis removed rather than emptied. See thefork-clause section below for why that is not a public-contract change.
surfacestays on every finding; it no longer selects a vocabulary.The fork clause did not fire, and here is the measurement
The dispatch carries a stop condition: if collapsing the split changes a
published contract rather than a detector's internal vocabulary, stop and
report. It does not, and the evidence is repo-local and re-measurable:
npm view @objectstack/metadata-core versionsends at 17.2.0; there is nonewer published version.
git tag --contains ba2ffbcaf— the commit that first addedform-predicate-root-policy.ts— returns nothing. It is in no release tag.(
.changeset/artifact-unbound-form-predicate-root-notice.md,.changeset/form-predicate-root-vocabulary-per-surface.md) are stillpending in
.changeset/, so the feature has never shipped.packages/metadata/src/plugin.ts(usesthe VIEW and FIELD lists, never
FIELD_ONLY_…) and this module's own test.So no upgrader can hold
FIELD_ONLY_BOUND_PREDICATE_ROOTS, and this is the lastmoment at which removing it is free. Shipping it as an empty constant named
FIELD_ONLY_…would have created the published-contract problem the forkclause is watching for, not avoided it.
Why the vocabulary is no longer justified by quoting the contract
The card flagged that this module already paid for this mistake once on the
FIELD half. It has now been wrong twice, both times by transcribing prose that
was faithful and stale. Prose is a transcription of a renderer, so it can only
ever lag one. Membership is therefore stated as the mechanism — a root is bound
on a surface iff some renderer threads a scope carrying it into that surface's
evaluator — with the threading site named per entry (objectui#6010 for the
field slot; objectui#6110's
isSectionVisiblethreading plus objectui#6111'ssection-dividerpseudo-field for the section slot). That is refutable by goingand looking, which a quoted sentence is not.
And the guard is mechanical rather than editorial: the module's test now reads
the live
.describe()text ofFormFieldSchema.visibleWhenandFormSectionSchema.visibleWhenout of@objectstack/spec/uiand fails when itstops agreeing with the list. A helper turns an unreadable accessor into a RED
test rather than an
undefinedthat would satisfy every assertion beneath it.The stale verbatim quote in the test, handled without deleting it
form-predicate-root-policy.test.tsquoted the retired sentence in a comment("No
current_userat section level — it is unbound here and the predicatewould fault open"). It is kept, as dated history and as the record of how
this file failed, and replaced as a mechanism by the live read above. Deleting
it would have removed the evidence and left the mechanism unfixed.
Blast radius, stated as the card states it
This is a notice, not a refusal — no parse change, no gate, no behaviour
change — and it only runs inside the versioned window
applyArtifactForwardConversionsopens. The cost removed is a false operatorsignal on legacy artifacts, not a broken runtime.
Declared widening beyond the two dispatched files
The dispatch fenced the file surface to site 1. Two files in
packages/metadataare in this diff anyway, both forced and both declared here:
src/plugin-unbound-form-predicate-roots.test.ts— its case "DOES flag thesame root at section level, and prints the section vocabulary there" pins
the exact behaviour this card removes, so the fix cannot land without it.
Skipping, disabling or re-baselining it is forbidden, so the expectation is
inverted in place with a comment naming the contract change, and a new
companion control ("still flags a genuinely unbound SECTION root, and prints
the section rule") keeps the section arm non-vacuous and re-pins the printed
section vocabulary at the door.
src/plugin.ts— comment only, no code change. The comment beside thenotice asserted "The bound vocabulary differs between a field slot and a
section slot (a field also binds the
current_userfamily, objectui#6010)",which is the same stale claim this card is about, one file over. No open PR
touches either file (checked against the 29 open PRs at the time of writing).
Verification
Gate family derived mechanically, not recalled:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, re-derivedafter merging
origin/mainso the answer came from a non-stale tree (the firstderivation warned STALE TREE; the two families are identical). Exit codes were
captured before any pipe.
Green (each quoting its own verdict line, not a bare exit code):
pnpm --filter @objectstack/metadata-core typecheck—tsc --noEmit && tsc --noEmit -p tsconfig.test.json, clean. Both edited files are genuinely inside that program:tsc -p tsconfig.test.json --listFileslistsform-predicate-root-policy.tsandform-predicate-root-policy.test.ts.pnpm --filter @objectstack/metadata-core --filter @objectstack/metadata test— 15 files / 261 tests and 39 files / 679 tests, all passing, run at final commite7a04a708.check:nul-bytes,check:test-source-alias("OK — 72 packages with tests scanned"),check:cross-package-test-inputs,check-undeclared-dep-imports,check-adr-0087-registration,check-empty-changeset,check-changeset-no-major,check:changeset-gate-self-tests,check:objectui-changeset,check:pm-half-states,check:engine-double-contract,check:where-matcher,check:query-options-erasure,check:objectql-double-limit,check:durability-log-level,check:logger-receiver-detach,check:page-declaration-shape,check:slot-lookup,check:stack-collection-maps,check:published-files,check:type-source-resolution,check-comment-mask-adoption,check-keyed-text-bounds,check-plugin-teardown-shape,check-ci-filter-parity,check:type-check-coverage,check-shard-attestation,check-cross-package-test-inputs,docs-audit/check-affected-docs,docs-audit/check-drift-comment,release-rehearsal-clone --self-test— all exit 0.NOT MEASURED locally, each quoting its own refusal, none counted as a pass:
node scripts/pm/check-half-states.mjs— exit 3: "Nothing was swept: no issue was listed, no predicate (H1-H16) ran … It is not a clean board and it is not a dirty one — it is no reading at all." (container GitHub credential). The workflow-invoked half,pnpm check:pm-half-states, passes: "1551 cases pass".node scripts/check-test-completeness.mjs— exit 3: "the local reading for this gate is NOT MEASURED. It is not a red, and there is nothing here to fix."pnpm check:dual-build-cjs-loads— exit 3: "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ … This is NOT a pass: nothing was measured."pnpm check:type-check-debt— exit 1 on its prerequisite, not on a finding: "--re-measure cannot run: 44 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk."Declared narrowing. Both of the last two want the whole workspace built
(
turbo run build --filter='./packages/*' --filter='./packages/*/*'). That runwas attempted here under the shared verify lock and was SIGKILLed at 13m41s
(
Failed: @objectstack/spec#build,run failed: command exited (137)) on acontended shared box — and it left
packages/spec/distwithout its declarationfiles, which was detected and repaired by rebuilding the closure before any
reading below was taken. CI runs both with the closure built.
For the one gate that could actually have moved, the narrowing was replaced by a
direct measurement rather than left as a gap:
@objectstack/metadatais the onlypackage in this diff carried in the type-check DEBT ledger (89 frozen raw
errors). Measured on the repaired closure,
tsc --noEmit -p packages/metadata/tsconfig.jsonreports 89 raw errors — the ledger figure exactly, shrink-only ratchet
unmoved — and zero of them are in either file this PR edits.
Non-vacuity (ablation)
Mutation: the
current_userfamily removed from the single vocabulary inform-predicate-root-policy.ts. Script carriedtrap restore EXIT INT TERMwith an absolute
REPO_ROOT-anchored path.'current_user'occurrences in the file 4 to 0,
git hash-object596ed4d6…to626b8782….pnpm --filter @objectstack/metadata-core build, exit 0) because thedoor suite consumes
dist/. The preflight's--absentleg reports exit 1,and that is a marker-choice artifact rather than a stale dist:
current_usersurvives in
dist/index.d.ts/index.d.ctsbecause it appears in themodule's doc prose, which tsup carries into the declarations. The
executable outputs are the discriminator, and they moved exactly as
required — the mutated leg lists no
dist/index.jsand nodist/index.cjshit, the restored leg lists both.
form-predicate-root-policy.test.ts5 failed / 22 passed(every new and every inverted case among them), and the door suite
plugin-unbound-form-predicate-roots.test.ts4 failed / 6 passed.git hash-objectagainst the HEAD blob — back to596ed4d6…,git diff HEADfor the path empty — not by the restorecommand's exit code. Rebuilt again, preflight present leg "✓ marker present in
4 built files", and both suites returned to 27/27 and 10/10.
Changeset
.changeset/form-predicate-root-section-binds-current-user.md, gradedpatchfor both packages — honest for a correction to a detector's vocabulary that has
never shipped. It carries the FROM to TO line for the removed export and an
ADR-0087
not-required (unpublished)disposition.Not addressed here
#13077is open and owns sites 2-4 (content/docs/ui/views.mdx,content/docs/protocol/objectui/layout-dsl.mdx,packages/lint/scripts/check-doc-formula-expressions.mjs). Nothing in this PRtouches those paths.
Generated by Claude Code
Generated by Claude Code