Skip to content

docs(spec,lint): a form SECTION visibleWhen binds current_user too — re-measure the four stale text sites - #13074

Merged
os-trump merged 4 commits into
mainfrom
claude/issue-12914-current-user-binding-text
Aug 29, 2026
Merged

docs(spec,lint): a form SECTION visibleWhen binds current_user too — re-measure the four stale text sites#13074
os-trump merged 4 commits into
mainfrom
claude/issue-12914-current-user-binding-text

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#12914

Four current_user binding-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:generated all 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-sha on origin/main has since moved to 9602dc820450dda956843c6cfe5b329bcf88c757, so every leg was re-read at that pin rather than trusted from the card:

legat pin 9602dc82verdict
apps/console/src/components/FormPage.tsxusePredicateScope() at :1568, docblock :1543-1567; isSectionVisible takes predicateScope (:648-660, parameter at :652) and is called with it at :1849holds, line numbers included
the four section-divider copy sitesObjectForm.tsx:1222, SplitForm.tsx:388, ModalForm.tsx:718/:750, DrawerForm.tsx:596/:654 — each copies the authored section.visibleWhen onto the section-divider pseudo-fieldholds; small line drift from the card (it said :386 / :721 / :597)
packages/components/src/renderers/form/form.tsxusePredicateScope() at :1169, bound at :1229 / :1265 / :1990 / :2003holds
packages/plugin-form/src/sectionFields.ts:138-140 copies the object field's visibleWhen / readonlyWhen / requiredWhen onto the runtime form field — site 4's mechanismholds
apps/console/src/components/InternalFormRoute.tsx:81 is ExpressionProvider user={expressionUser} — publishes user, never featuresholds, and the features.* fence stands on it
packages/plugin-form/src/__tests__/sectionVisibleWhen-6111.test.tsxpresentholds

One prose drift in the card, recorded rather than silently absorbed: there is no symbol named buildExpressionScope at this pin. usePredicateScope() is useContext(PredicateScopeContext) (packages/react/src/hooks/useExpression.ts:53) and the scope object is built inline in ExpressionProvider as { 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.ts evaluates field requiredWhen / readonlyWhen and per-option visibleWhen, and that is the whole list — no form-view section or field visibleWhen anywhere 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.visibleWhen

describe(), before:

No current_user at section level — it is unbound here and the predicate would fault open.

describe(), after:

current_user (and the ADR-0068 aliases user / ctx.user / os.user) resolves here too — CLIENT-SIDE only: nothing server-side evaluates a form-view section visibleWhen, so a role test here hides the controls and protects no data (declare permission-set field-level security for that), and on the public /f/:slug route no host publishes a scope, so the root is unbound and the predicate faults open.

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_user DOES bind here section that names both discharged halves (the renderer threading the scope into isSectionVisible; the object-view chain carrying the key through the section-divider pseudo-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.visibleWhen

describe(), before:

Env: the live record plus the host predicate scope, which binds current_user — wider than field-level visibleWhen, which has no current_user.

describe(), after:

Env: the live record plus the host predicate scope, which binds current_user. The one VISIBILITY predicate the SERVER also enforces — the rule validator refuses a write of a value whose predicate is false — so a user-gated CHOICE belongs here.

JSDoc, before: "…field- and section-level rules go through evalFieldPredicate, which binds record + previous + parent and never current_user. Per-option is the one *When surface where a current_user test 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 *When surface": the write path also evaluates field readonlyWhen and requiredWhen, 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_GUIDANCE

Before: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 binds current_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.visibleWhen

Before:

the predicate faults and the renderer falls back to VISIBLE (resolveFieldRuleState evaluates visibility with fallback: true, and no server-side gate evaluates a field-level visibleWhen at all), leaving the field the test was meant to hide showing for everyone (#6146)

After:

the predicate no longer merely faults — and BOTH of its outcomes are wrong, in opposite directions. Under a host that publishes a predicate scope the renderer RESOLVES it (plugin-form's sectionFields copies this object rule onto the runtime form field and resolveFieldRuleState evaluates it with the host scope bound, objectui#6010) — so the control is hidden in that one form while NO server-side gate evaluates a field-level visibleWhen at all: the record still carries the value and every other reader still returns it, a SILENT enforcement gap. Where no host publishes a scope (the console public /f/:slug route, and every non-form reader) the root is unbound, the predicate faults and the renderer falls back to VISIBLE (resolveFieldRuleState evaluates visibility with fallback: true), leaving the field the test was meant to hide showing for everyone (#6146). The gap is the WORSE of the two — a visible fail-open gets reported, and a silent one does not

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 section describe() stays verbatim. That root is unbound on BOTH standalone routes because InternalFormRoute publishes only user, so nothing about it moved. Proof by grep, both ends:

$ grep -c 'No `features.*` on ANY form-view predicate — refused at parse (ruled 2026-08-27): unbound on the standalone form routes, where the predicate would fault open.' packages/spec/src/ui/view.zod.ts
1 # at HEAD
1 # at origin/main, same command through git show

Fence 2 — FIELD_RULE_BOUND_ROOTS is not relaxed. Proof by diff absence: the whole branch diff of validate-expressions.ts contains exactly one line naming it, and that line is an ADDED comment forbidding the relaxation.

$ git diff origin/main...HEAD -- packages/lint/src/validate-expressions.ts | grep '^[+-].*FIELD_RULE_BOUND_ROOTS'
+ * ⛔ This is not a reason to relax {@link FIELD_RULE_BOUND_ROOTS}. The

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 HEAD under 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 to src and never to dist.

lint old-sentence present : 1 (want >=1) spec old-clause present : 1 (want 1)
lint new-sentence absent : 0 (want 0) spec new-clause absent : 0 (want 0)
blob differs from HEAD : lint=yes spec=yes
× `visibleWhen` — names the SILENT enforcement gap, the outcome under a bound scope
AssertionError: expected '`visibleWhen` reads `current_user`, b…' to match /RESOLVES/
Tests 1 failed | 230 passed (231)
× it points at per-option `visibleWhen`, and that predicate really parses HERE
AssertionError: expected 'Unrecognized key(s) on this select op…' not to contain 'the one `*When` surface'
Tests 1 failed | 40 passed (41)

Restore leg proved the same way, not by its exit code: git diff HEAD empty, git status --porcelain empty, 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 pins FIELD_RULE_BOUND_ROOTS beside the wording.
  • editability-boundary.test.ts — keeps toContain('current_user (ADR-0068)'), adds the negative not.toContain('the one *When surface') and a positive on the clause the prescription now rests on.

⚠️ One trap paid for in a round trip, recorded because the file already documents its sibling: the first draft spelled the module as sectionFieldswith its .ts extension inside the message STRING, and #5017's receiver scan strips comments but not strings, so it registered sectionFields as a read receiver and turned covers every receiver in the source that is not explicitly excused red. Spelled without the extension, exactly as the *.form spelling two cells down already is, with a comment at the site and a not.toMatch(/sectionFields\.ts/) pin so it cannot come back.

Generated artifacts

gen:docs regenerated two reference pages, not one: content/docs/references/ui/view.mdx (5 rows — 3 section rows plus the 2 SelectOption rows that view embeds) and content/docs/references/data/field.mdx (2 SelectOption rows). The card and the dispatch named only view.mdx; the second file is the generator's own consequence of the site-2 describe() edit, not a scope expansion. 7 changed lines in total, every one a visibleWhen table row.

check:doc-authoring red-lined the first draft of the section describe() for carrying objectui#6110 + #6111 — internal issue ids are refused in customer-facing spec text, and .describe() prose projects into content/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:

✓ check:authorable-surface authorable-surface/ + authorable-defaults/ (+ its .base.json anchor) + JSON schemas
✓ check:api-surface api-surface/
✓ check:docs content/docs/references/**
✓ check:liveness liveness/state-counts.md
✓ check:strictness-ledger docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
… and the other 9, all ✓

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-docs

Plus, 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.

⚠️ Two of those gates first returned exit 1 reading PREREQUISITE NOT MET — @objectstack/lint is 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

@objectstack/lint — full package suite Test Files 81 passed (81) Tests 2302 passed (2302)
@objectstack/spec — the 9 test files that read
FormSectionSchema/SelectOptionSchema Test Files 9 passed (9) Tests 781 passed (781)
@objectstack/metadata-core — form-predicate-root-policy.test.ts Tests 26 passed (26)

Typecheck

pnpm --filter @objectstack/spec typecheck exit 0 (tsc --noEmit + check:scripts-typecheck +
check:test-typecheck: OK — @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.json;
55 file(s) / 263 error(s) held in test-typecheck-debt.json)
pnpm --filter @objectstack/lint typecheck exit 0

Coverage proof, by --listFiles rather than by assumption — because "typecheck green" is not automatically a statement about the files this PR edited:

programedited filein program?
packages/spectsc --noEmitui/view.zod.ts, data/field.zod.ts, shared/editability-boundary.ts3 of 3
packages/spectsconfig.test.jsonshared/editability-boundary.test.tsyes
packages/linttsc --noEmitsrc/validate-expressions.tsyes
packages/linttsc --noEmitsrc/validate-expressions.test.tsNO

That last row is honest rather than green: packages/lint/tsconfig.json excludes **/*.test.ts, so its typecheck says 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 at origin/main and 0 errors at this HEAD, so the added test code cannot move packages/lint's TEST_DEBT ratchet count.

Lintpnpm 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, whose BOUND_FORM_VIEW_PREDICATE_ROOTS is 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-level current_user predicate. 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

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 5 documentable anchor(s).

14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx(via visibleWhen (symbol))
  • content/docs/automation/flows.mdx(via visibleWhen (symbol))
  • content/docs/data-modeling/field-types.mdx(via visibleWhen (symbol))
  • content/docs/data-modeling/fields.mdx(via visibleWhen (symbol))
  • content/docs/data-modeling/formulas.mdx(via visibleWhen (symbol))
  • content/docs/data-modeling/validation-rules.mdx(via visibleWhen (symbol))
  • content/docs/deployment/environment-variables.mdx(via visibleWhen (symbol))
  • content/docs/protocol/objectui/actions.mdx(via /f/:slug (route), /forms/:name (route))
  • content/docs/protocol/objectui/layout-dsl.mdx(via FormSectionSchema (symbol), visibleWhen (symbol), /f/:slug (route), /forms/:name (route))
  • content/docs/ui/actions.mdx(via /forms/:name (route))
  • content/docs/ui/forms.mdx(via /f/:slug (route), /forms/:name (route))
  • content/docs/ui/pages.mdx(via visibleWhen (symbol))
  • content/docs/ui/public-data-collection.mdx(via /forms/:name (route))
  • content/docs/ui/views.mdx(via visibleWhen (symbol), /f/:slug (route), /forms/:name (route))

5 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via visibleWhen (symbol))
  • content/docs/releases/v12.mdx(via visibleWhen (symbol))
  • content/docs/releases/v15.mdx(via FormSectionSchema (symbol), visibleWhen (symbol))
  • content/docs/releases/v16.mdx(via visibleWhen (symbol))
  • content/docs/releases/v17.mdx(via visibleWhen (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 269167fefd8f7ce6f5c3ee7cbffb5be73ede426epackageMentionDocs.

Which tree this was computed on

This run read content/docs from be9c4b3fc2d8ebf05cc041a4ef82b0153a186a60 — the merge of head ce3a9732b1a56026d825c51a78762934a5b9d62e into base 269167fefd8f7ce6f5c3ee7cbffb5be73ede426e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 269167fefd8f7ce6f5c3ee7cbffb5be73ede426e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-trumpos-trump added needs:contract-review and removed documentation Improvements or additions to documentation tests tooling protocol:ui protocol:data labels Aug 29, 2026 — with Claude
@os-trump
os-trump marked this pull request as ready for review August 29, 2026 03:22
@os-trump
os-trump enabled auto-merge August 29, 2026 03:22
@os-trump
os-trump added this pull request to the merge queueAug 29, 2026
Merged via the queue into main with commit f887e52Aug 29, 2026
53 checks passed
@os-trump
os-trump deleted the claude/issue-12914-current-user-binding-text branch August 29, 2026 03:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Four more current_user binding-text sites are stale — including the form SECTION slot, whose verdict flipped when objectui#6110 / #6111 landed

2 participants

@os-trump@claude