docs(spec,lint): a form SECTION visibleWhen binds current_user too — re-measure the four stale text sites - #13074
Conversation
…ck:doc-authoring)
📓 Docs Drift CheckThis PR changes 2 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 126 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 be9c4b3fc2d8ebf05cc041a4ef82b0153a186a60 && git checkout be9c4b3fc2d8ebf05cc041a4ef82b0153a186a60
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 269167fefd8f7ce6f5c3ee7cbffb5be73ede426e ce3a9732b1a56026d825c51a78762934a5b9d62e && git checkout -B drift-repro 269167fefd8f7ce6f5c3ee7cbffb5be73ede426e && git merge --no-ff ce3a9732b1a56026d825c51a78762934a5b9d62e
node scripts/docs-audit/affected-docs.mjs --json 269167fefd8f7ce6f5c3ee7cbffb5be73ede426e
|
Fixes #12914
Four
current_userbinding-text sites re-measured. Text only — no schema key, no verdict, no runtime behaviour moves; the authorable surface, the API surface and every ratchet baseline are unchanged (check:generatedall 14 green, below).The claim these sites carried was true when it was written. Two objectui PRs discharged both halves of the reason it held, and the re-measurement is the deliverable.
Premise re-measured at the pin this repo NOW builds against
The card measured objectui at
f241a4d..objectui-shaonorigin/mainhas since moved to9602dc820450dda956843c6cfe5b329bcf88c757, so every leg was re-read at that pin rather than trusted from the card:9602dc82apps/console/src/components/FormPage.tsxusePredicateScope()at:1568, docblock:1543-1567;isSectionVisibletakespredicateScope(:648-660, parameter at:652) and is called with it at:1849ObjectForm.tsx:1222,SplitForm.tsx:388,ModalForm.tsx:718/:750,DrawerForm.tsx:596/:654— each copies the authoredsection.visibleWhenonto thesection-dividerpseudo-field:386/:721/:597)packages/components/src/renderers/form/form.tsxusePredicateScope()at:1169, bound at:1229/:1265/:1990/:2003packages/plugin-form/src/sectionFields.ts:138-140copies the object field'svisibleWhen/readonlyWhen/requiredWhenonto the runtime form field — site 4's mechanismapps/console/src/components/InternalFormRoute.tsx:81isExpressionProvider user={expressionUser}— publishesuser, neverfeaturesfeatures.*fence stands on itpackages/plugin-form/src/__tests__/sectionVisibleWhen-6111.test.tsxOne prose drift in the card, recorded rather than silently absorbed: there is no symbol named
buildExpressionScopeat this pin.usePredicateScope()isuseContext(PredicateScopeContext)(packages/react/src/hooks/useExpression.ts:53) and the scope object is built inline inExpressionProvideras{ current_user: user, user, ctx: { user }, os: { user }, app, data, features }. What the card asserted about what the published scope binds is exactly right; only the function name and line span were stale.The write-path half was re-checked in this repo at the base commit:
packages/objectql/src/validation/rule-validator.tsevaluates fieldrequiredWhen/readonlyWhenand per-optionvisibleWhen, and that is the whole list — no form-view section or fieldvisibleWhenanywhere on it. That is what lets the CLIENT-SIDE-only qualification carry verbatim from the field text to the section text.The prose delta, per site
Site 1 —
packages/spec/src/ui/view.zod.ts,FormSectionSchema.visibleWhendescribe(), before:
describe(), after:
JSDoc, before: "— and, as there, no
current_user: it is unbound at this level, so such a predicate faults and falls back to visible (#6146)."JSDoc, after: a
current_userDOES bind here section that names both discharged halves (the renderer threading the scope intoisSectionVisible; the object-view chain carrying the key through thesection-dividerpseudo-field), states it is a re-measurement and not a relaxation, then carries the two field-text qualifications as the same numbered pair the field block uses — rendering rule never authorization, and the scope belongs to the HOST so it is empty on/f/:slug.Site 2 —
packages/spec/src/data/field.zod.ts,SelectOptionSchema.visibleWhendescribe(), before:
describe(), after:
JSDoc, before: "…field- and section-level rules go through
evalFieldPredicate, which bindsrecord+previous+parentand nevercurrent_user. Per-option is the one*Whensurface where acurrent_usertest actually resolves."JSDoc, after: the same evaluators, with the vocabulary claim corrected (a different evaluator, but handed the same host scope) and the grounding moved onto what survives — ENFORCEMENT, not exclusivity.⚠️ Note the replacement says "the one VISIBILITY predicate the server also enforces", not "the one
*Whensurface": the write path also evaluates fieldreadonlyWhenandrequiredWhen, so the unqualified form would have been a second false exclusivity claim in the same sentence that removed the first.Site 3 —
packages/spec/src/shared/editability-boundary.ts,SELECT_OPTION_EDITABILITY_GUIDANCEBefore:
predicate — the one*Whensurface that also bindscurrent_user(ADR-0068), so an option can be withheld per record or per role, and the rule validator refuses a write…After:
predicate — it bindscurrent_user(ADR-0068), so an option can be withheld per record or per role, and the rule validator refuses a write…A trim, as the card called it: the durable clause was already the next one along.
Site 4 —
packages/lint/src/validate-expressions.ts,FIELD_RULE_SLOT_CONSEQUENCE.visibleWhenBefore:
After:
The fault-open leg is kept rather than replaced because it is still exactly what happens wherever no host publishes a scope. The Axis-1 doc block above the cell moves with it and now says the same two-legged thing instead of "the sentence was correct".
The two contract fences, proved rather than asserted
Fence 1 — the
features.*refusal sentence in the sectiondescribe()stays verbatim. That root is unbound on BOTH standalone routes becauseInternalFormRoutepublishes onlyuser, so nothing about it moved. Proof by grep, both ends:Fence 2 —
FIELD_RULE_BOUND_ROOTSis not relaxed. Proof by diff absence: the whole branch diff ofvalidate-expressions.tscontains exactly one line naming it, and that line is an ADDED comment forbidding the relaxation.The declaration itself is byte-identical at both ends (
export const FIELD_RULE_BOUND_ROOTS = ['record', 'previous', 'parent'] as const;), and a new pin asserts it beside the new wording so the two cannot drift apart.Pins
Both pins the card named moved with the wording, and both were shown red-if-reverted rather than argued.
Reverse verification, on the committed state, restoring both files from
HEADunder a trap with absolute paths. The mutation was proved on disk before anything was read (injected text present, deleted text absent, blob hash differs from the HEAD blob); no rebuild leg is owed because both tests import their subject by RELATIVE specifier inside their own package, so vitest resolves tosrcand never todist.Restore leg proved the same way, not by its exit code:
git diff HEADempty,git status --porcelainempty, and both worktree blob hashes equal to their HEAD blobs (55462338…/3023ea6f…).New assertions added beside the moved ones:
validate-expressions.test.ts— the fault-open pin keeps/falls back to VISIBLE/and/showing for everyone/(that leg is still real), and a new sibling test pins the half the re-measurement ADDED:/RESOLVES/, the named mechanism,/SILENT enforcement gap/,/every other reader still returns it/,/WORSE of the two/. Without it a revert could hide behind the surviving sentence — the exact blind spot the block comment above it already owns up to for the original per-slot split. A third test pinsFIELD_RULE_BOUND_ROOTSbeside the wording.editability-boundary.test.ts— keepstoContain('current_user(ADR-0068)'), adds the negativenot.toContain('the one*Whensurface')and a positive on the clause the prescription now rests on.sectionFieldswith its.tsextension inside the message STRING, and #5017's receiver scan strips comments but not strings, so it registeredsectionFieldsas a read receiver and turnedcovers every receiver in the source that is not explicitly excusedred. Spelled without the extension, exactly as the*.formspelling two cells down already is, with a comment at the site and anot.toMatch(/sectionFields\.ts/)pin so it cannot come back.Generated artifacts
gen:docsregenerated two reference pages, not one:content/docs/references/ui/view.mdx(5 rows — 3 section rows plus the 2SelectOptionrows that view embeds) andcontent/docs/references/data/field.mdx(2SelectOptionrows). The card and the dispatch named onlyview.mdx; the second file is the generator's own consequence of the site-2describe()edit, not a scope expansion. 7 changed lines in total, every one avisibleWhentable row.check:doc-authoringred-lined the first draft of the sectiondescribe()for carryingobjectui#6110 + #6111— internal issue ids are refused in customer-facing spec text, and.describe()prose projects intocontent/docs/references/**. The ids were stripped from the string and kept in the JSDoc comment beside it, which the gate does not read. Green after.Verification — exit codes captured before any pipe, verdict lines quoted
Everything below ran at
ce3a9732b, the final commit on this branch, with the workspace built.Generated artifacts —
pnpm --filter @objectstack/spec check:generated, exit 0, all 14 green with the regenerated pages in the diff:The derived gate family —
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, re-derived at the final commit (it reads the change set from the merge base itself; the list is identical to the one derived at the first commit, so nothing appeared late). 32 gates, all 32 run, all green:check:authorable-surface·check:changeset-gate-self-tests·check:cross-package-test-inputs·check:doc-anchors·check:doc-authoring·check:doc-formula-expressions·check:doc-security-posture·check:docs·check:docs-audit-scope·check:docs-redirects·check:docs-single-h1·check:empty-state·check:generated·check:liveness·check:merge-driver·check:objectql-double-limit·check:objectui-changeset·check:page-declaration-shape·check:pm-half-states·check:published-files·check:published-readme-links·check:quick-reference-counts·check:react-page-adapter-contract·check:role-word·check:skill-examples·check:skill-refs·check:slot-lookup·check:spec-parsed-alias·check:strictness-ledger·check:test-source-alias·check:type-source-resolution·check:variant-docsPlus, from the same tool's wider leads:
check:nul-bytes,check:engine-double-contract,check:where-matcher,check:query-options-erasure,check:dispatcher-error-vocabulary,check:type-check-coverage— all green.@objectstack/lintis not built, and one returned it for an unbuilt@objectstack/client-react. Those are NOT MEASURED, not findings; both were re-run to a real verdict after the missing packages were built, and only the real verdicts are counted above.Tests
Typecheck
Coverage proof, by
--listFilesrather than by assumption — because "typecheck green" is not automatically a statement about the files this PR edited:packages/spectsc --noEmitui/view.zod.ts,data/field.zod.ts,shared/editability-boundary.tspackages/spectsconfig.test.jsonshared/editability-boundary.test.tspackages/linttsc --noEmitsrc/validate-expressions.tspackages/linttsc --noEmitsrc/validate-expressions.test.tsThat last row is honest rather than green:
packages/lint/tsconfig.jsonexcludes**/*.test.ts, so itstypechecksays nothing at all about the test file this PR edits. What covers it instead: the vitest run above executed it (231 assertions in that file), and its typecheck impact was measured directly — compiled alone against the package's compiler options with the closure built, the file reports 0 errors atorigin/mainand 0 errors at this HEAD, so the added test code cannot movepackages/lint's TEST_DEBT ratchet count.Lint —
pnpm lint(eslint . --no-inline-config, whole repo, no narrowing): exit 0.Not run locally, and named rather than left implied:
check:type-check-debt --re-measure, whose ratchet half needs the entire workspace closure built. Its structural half (check:type-check-coverage) is green above, this PR adds no new test file and no new package, and the direct 0-vs-0 measurement in the row above bounds the only count it could have moved.Out of scope, filed rather than swept
Three further consumers of the retired section claim were found while re-measuring, all outside the dispatched file surface, none touched: #13072. The one worth a reviewer's attention is
packages/metadata-core/src/form-predicate-root-policy.ts, whoseBOUND_FORM_VIEW_PREDICATE_ROOTSis derived from the very sentence site 1 replaces — a live boot-notice detector that will start crying wolf on legacy artifacts carrying a legitimate section-levelcurrent_userpredicate. It is a notice and not a refusal, so nothing breaks; but that module's own docblock records it paying for this exact mistake once already on the FIELD half. Collapsing its per-surface split is a design decision this PR deliberately did not make. Its test still passes (26/26) because the module was not changed; what has gone stale there is a comment quoting the old sentence, and no gate reads comments.Generated by Claude Code
Generated by Claude Code