Skip to content

feat(spec): form-view predicates loudly reject the features.* scope root - #12849

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-12665-features-root-rejection
Aug 28, 2026
Merged

feat(spec): form-view predicates loudly reject the features.* scope root#12849
os-sales merged 1 commit into
mainfrom
claude/issue-12665-features-root-rejection

Conversation

@os-sales

@os-salesos-sales commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12665

Session of record: https://claude.ai/code/session_01JvjTCjJQn9zSTXEhUKgT7s (os-dev dispatch under the domain:spec seat).

What / why

Implements the maintainer ruling of 2026-08-27 (decision-inbox batch 2, verbatim: 「同意」 — adopting Option B on objectui#6262): form views may not name the features.* scope root in predicates, and the exclusion is declared and enforced at the authoring/publish door with a loud rejection.

One authored form view is served on two kinds of route, and a features.* predicate got two verdicts from the same text: inside an app (/apps/:appName/*) the root resolves against the real auth-config flags; on the standalone form routes (/forms/:name, public /f/:slug) no app context exists, the root is UNBOUND, the predicate faults — and visibleWhen's fault fallback is visible, so the field or section a feature flag was meant to hide was shown to everyone (fail-open, on an access-shaped key). The ruling narrows the vocabulary instead of building auth-config parity machinery for zero consumers.

Day-of premise re-verification (dev's first action, per the card)

Re-ran the objectui#6262 measurement against objectui origin/main @ 38a123cac (2026-08-28, before any code): across apps/examples/content/ over json/yml/yaml/mdx — positive control visibleWhen: 18 hits; target features.: 3 raw hits, all three the known false positives (two English prose sentences, one JavaScript string inside a code-preview widget). Zero authored features.* form-view predicates — the ruling's premise holds.

Changes

  • packages/spec/src/ui/view.zod.tscheckFormViewPredicateFeaturesRoot scanner (CEL string literals stripped, then features in root position; member access like record.features.x stays legal) wired into FormViewSchema's existing superRefine, covering section-level visibleWhen, field-level visibleWhen at any nesting depth, and per-option visibleWhen authored inline in the form view — both sections and the legacy groups bucket (pre-fold), the deprecated visibleOn alias spellings (refused post-fold), every form type including split, and the flattened runtime form overlay (refinement carried by .extend()). The refusal is a prescriptive custom parse issue naming the root, the surface, the fail-open reason and the ruling. Error identity: this rides the standard Zod-issue validation channel (same as the existing pane and submitBehavior.url refusals) and surfaces on the wire through the standard validation envelope — no new top-level error.code string is introduced, so nothing enters the ADR-0112 ledger.
  • ADR-0087: semantic migration entry 18.ui-form-view-predicate-features-root-refused + gen:migration-registry regeneration.
  • Docs: boundary callout + updated visibleWhen rows in content/docs/ui/views.mdx; describe() strings updated (generated content/docs/references/ui/view.mdx regenerated via gen:docs).
  • Changeset: @objectstack/specminor (accept-set narrowing, per the repo convention — same level as the FormField constraint-shape narrowing; check:changeset-no-major respected).
  • Tests: packages/spec/src/ui/view-form-features-root.test.ts — 9 refusal cases pinning code/path/message identity (bare-string + envelope, section/field/nested/per-option, groups, visibleOn alias, bracket + bare-root + call-arg forms, type: 'split'), 5 positive controls (permitted roots, record.features.x member access, longer identifiers, string literals, AST-only envelope's documented pass), 2 app-context pins (page component and bulk-action predicates naming features.* still parse — that surface keeps features.* exactly as-is).

⛔ Scope guard: no other predicate surface changed. objectui#6262 remains open — it is unlocked by this landing, not closed by it (that card is pm:blocked on the card this PR fixes, and it is resolved over on the objectui side only after its own re-verification).

Verification (all readings at HEAD a363c042, after the final commit)

  • Gate union derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (7 changed paths vs merge-base f907fbe9e): 37 path-derived families + 6 convention-triggered (new-test-file kind). All measured families green, including check:authorable-surface, check:generated (14/14 artifacts, printed "All 14 generated artifacts are up to date"), check:docs, check:migration-registry, check:spec-changes, check:upgrade-guide, check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check:type-check-coverage, and check:type-check-debt (printed "OK — 31 ledger entries re-measured … none above its recorded number", run after building the full packages closure exactly as its own refusal prescribes).
  • NOT MEASURED (each with the gate's own printed line): node scripts/pm/check-half-states.mjs (half-state-patrol flavor) — exit 3, printed "Nothing was swept … it is no reading at all" (needs a GitHub credential this container does not hold; the lint.yml flavor pnpm check:pm-half-states = self-test ran green); check:react-declaration-parity — check:generated's own printed exclusion ("needs MANIFEST … objectui's registry-inputs dump", an input this repo cannot produce; CI owns it).
  • Spec suite: 437 files / 11576 tests, all green. pnpm --filter @objectstack/spec typecheck green (main + scripts + test layer; the new test file confirmed inside the tsconfig.test.json population via tsc --listFiles, 1 hit). Repo-wide pnpm lint (eslint, --no-inline-config) exit 0 at the same HEAD. check:nul-bytes green (7141 files).
  • Fixture sweep by the rule's consumption radius: repo-wide grep for features. in visibleWhen/visibleOn fixtures — the only hits are action-param fixtures (action.test.ts), an app-context surface this PR does not touch and pins untouched.
  • Ablation (guard added ⇒ guard removed must let the bad predicate pass): scanner neutralized via an anchored injected early-return, mutation proved on disk (grep -c of the marker = 1 at the anchor); refusal suite on the mutated tree: 9 failed / 7 passed — exactly the nine refusal cases turned red (bad predicates parsed successfully), the seven positive/app-context controls stayed green; restore via git checkout HEAD -- path, proved by blob equality (git hash-object = HEAD blob bc2074d3…, marker count 0, git diff HEAD empty); restored tree re-run: 16/16 green. No rebuild was needed for either leg — the suite imports ./view.zod as an in-package relative source import, so no dist/exports resolution is in play (stated per the ablation contract); the spec package was nonetheless rebuilt afterwards because the mutate+restore touched the source mtime and the dist-freshness preflights (check:api-surface, check:skill-examples) refuse on mtime — both green after rebuild.

Generated by Claude Code

…oot (#12665)
One authored form view is served on two kinds of route, and a features.*
predicate got two verdicts from the same text: bound to real auth-config
flags inside an app, UNBOUND on the standalone form routes (/forms/:name,
public /f/:slug) - where the predicate faults and visibleWhen fails OPEN,
showing the feature-gated field to everyone. Ruled 2026-08-27 on
objectui#6262 (option B): the vocabulary is narrowed at the authoring door.
- FormViewSchema superRefine scans every predicate the form view carries
(section-level, field-level at any depth, per-option) for features in
root position, string literals stripped; prescriptive refusal names the
root, the surface, the fail-open reason and the ruling.
- ADR-0087 semantic entry ui-form-view-predicate-features-root-refused;
registry regenerated.
- Docs: ui/views.mdx boundary callout + describe() strings (references
page regenerated).
- App-context predicate surfaces are untouched and pinned untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvjTCjJQn9zSTXEhUKgT7s
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling labels Aug 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 18 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 6f0fec3d0b8046f9f171ff157a7401fa5293d53f.

5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 4 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 6f0fec3d0b8046f9f171ff157a7401fa5293d53fpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 25add2ebb700d39867c551e0ec80dfa40944b89a — the merge of head a363c0426b606a52be84522c7f12f516752b57bf into base 6f0fec3d0b8046f9f171ff157a7401fa5293d53f, 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 25add2ebb700d39867c551e0ec80dfa40944b89a && git checkout 25add2ebb700d39867c551e0ec80dfa40944b89a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6f0fec3d0b8046f9f171ff157a7401fa5293d53f a363c0426b606a52be84522c7f12f516752b57bf && git checkout -B drift-repro 6f0fec3d0b8046f9f171ff157a7401fa5293d53f && git merge --no-ff a363c0426b606a52be84522c7f12f516752b57bf
node scripts/docs-audit/affected-docs.mjs --json 6f0fec3d0b8046f9f171ff157a7401fa5293d53f

⚠️ 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 6f0fec3d0b8046f9f171ff157a7401fa5293d53f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-sales
os-sales marked this pull request as ready for review August 28, 2026 04:32
@os-sales
os-sales enabled auto-merge August 28, 2026 04:32
@os-sales
os-sales added this pull request to the merge queueAug 28, 2026
Merged via the queue into main with commit daeaaf9Aug 28, 2026
44 checks passed
@os-sales
os-sales deleted the claude/issue-12665-features-root-rejection branch August 28, 2026 04:54
os-sales pushed a commit that referenced this pull request Aug 28, 2026
…12849)
os-regen-merge.sh step 4: gen:migration-registry rebuilds registry.ts with
BOTH step-18 populations (#12849's ui-form-view-predicate-features-root-refused
semantic entry beside this PR's retirement + semantic entries), the retirement's
manifest and authorable-surface deletions are re-applied over main's side (the
merge takes main's copy of os-regen artifacts by design; the #4725/#4650 proofs
re-verify against the new base), and the full chain regenerates green:
check:generated 14/14, check:llms-txt 208 schemas, the five touched spec
suites, client typecheck and the resolver + unwrap pin suites all pass on the
merged tree.
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:uisize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: form-view predicates must loudly reject the features.* root — ruled vocabulary narrowing (from objectui#6262)

2 participants

@os-sales@claude