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

os-sales merged 1 commit into
mainfrom
claude/issue-12665-features-root-rejection

Conversation

@os-sales

@os-sales os-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.ts — checkFormViewPredicateFeaturesRoot 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/spec minor (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-actions github-actions Bot 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 6f0fec3d0b8046f9f171ff157a7401fa5293d53f → packageMentionDocs.

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 queue Aug 28, 2026
Merged via the queue into main with commit daeaaf9 Aug 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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 1, 2026
…ds; retire the false PackageRollbackResponseSchema (objectstack-ai#12038, ruling 1C·2C·3A·4A·5A) (objectstack-ai#12848)

* spec: retire PackageRollbackResponseSchema and the rollbackPackage contract binding (objectstack-ai#12038 3A)

The schema declared a VERSION rollback ({ success, restoredVersion?,
message? }) while PackageApiContracts.rollbackPackage bound it to the live
POST /api/v1/packages/:packageId/rollback path, which the dispatcher serves
with rollbackToPackageCommit — the ADR-0067 COMMIT rollback, a different
operation with a different result. Retired through the ADR-0087 discipline:
RETIRED_DEFS_BY_MAJOR entry api/PackageRollbackResponse, D3 semantic entry
package-rollback-response-retired, manifest key and authorable-surface
baseline lines released with their registered proof (objectstack-ai#4725 / objectstack-ai#4650), runtime
namespace-probe pins in package-api.test.ts. PackageRollbackRequestSchema
stays published as ruled; the true commit-rollback contract follows in the
next commit, after this retirement per the ruling's sequencing.

Co-authored-by: Claude <noreply@anthropic.com>

* spec: bind the meta history/diagnostics and package lifecycle response contracts (objectstack-ai#12038)

The twelve describe-only transcriptions from the recorded ruling
(1C/2C/3A/4A/5A), each transcribed from the return type its producer already
declares inline: six meta.* payloads in api/protocol.zod.ts (listDrafts,
getMetaDiagnostics, findReferencesToMeta, rollbackMetaItem, diffMetaItem,
plus the ruling-1C opaque GetPublishedMetaItemResponseSchema), the package
lifecycle family in the new api/package-lifecycle.zod.ts (discardDrafts,
listCommits with the handler-minted commits wrapper, revertCommit, the true
commit-rollback RollbackToPackageCommitResponseSchema, the ruling-4A
fixed-keys-plus-catchall PackageExportManifestSchema, adoptOrphans,
duplicate), and the resolved book tree as Zod beside its interfaces in
system/book.zod.ts. Ruling 5A re-exports (PackagePublishResultSchema,
ResolvedBookSchema family) land in the /api namespace the ledger resolver
searches — never a second copy. meta.migrateStored stays unbound, documented
(ruling 2C). Generated artifacts regenerated by the spec build.

Co-authored-by: Claude <noreply@anthropic.com>

* runtime/rest/client: name the objectstack-ai#12038 response contracts in the ledgers and bind the SDK

Fill responseSchema on the 18 boundable ledger rows across both ledgers,
each row stating which surface's envelope it describes (the dispatcher wraps
{ success, data }; the REST server answers the payload bare — the three
dual-mounted meta routes carry one note per surface). meta.migrateStored's
two rows document the ruling-2C deliberate unbinding instead. Conformance
suites land with the rows (spec api/protocol.test.ts, the new
api/package-lifecycle.test.ts, system/book.test.ts — the objectstack-ai#3877
no-row-without-conformance rule, house capture pattern).

The client SDK binds 16 of the 17 methods to the published payload types
(migrateStored stays any, documented per 2C; getPublished binds to unknown
per 1C), replaces the four invented test mocks (getDiagnostics, getBookTree,
rollbackItem, diffItem) with producer-true shapes, flips the objectstack-ai#11925 rollback
negative guard to guard the new commit-rollback truth, adds the
returnTypePrecisionPins12038 type-level pins, and pins the unwrapResponse
mis-unwrap hazard (survey §8.2) so no bound payload can ever declare both a
boolean success and a data key.

Co-authored-by: Claude <noreply@anthropic.com>

* spec/docs: regenerate artifacts for the new contracts; add the objectstack-ai#12038 changeset

api-surface, export-origins, reference docs and the strictness ledger
regenerated by check:generated --fix (only the artifacts it proved stale);
the ResolvedBook type family re-exported on /api so the generated page's
import line resolves. The changeset carries the breaking FROM-to-TO mapping
and the ADR-0087 disposition (registered package-rollback-response-retired).

Co-authored-by: Claude <noreply@anthropic.com>

* spec: keep the package-lifecycle header inside the description corpus pins

The file docblock opened at heading level 1 (the objectstack-ai#12249 demotion pin counts
exactly the 38 files that predate it) and split a code span across lines,
leaving a same-directory source path as plain text for the objectstack-ai#6484 rule. Open
at level 2 and keep the span on one line; reference docs regenerated.

Co-authored-by: Claude <noreply@anthropic.com>

* spec/docs: satisfy the ADR-0122 alias convention and the quick-reference total for the new contracts

The 13 new response types gain their Parsed aliases (check:spec-parsed-alias;
all are isomorphic — no defaults or transforms — but the paired-alias route
keeps the family uniform with its package-api siblings), the API Protocol
quick-reference heading's M rises to the 31 pages the reference tree now
publishes, and the spec artifacts are regenerated for the new type exports.

Co-authored-by: Claude <noreply@anthropic.com>

* spec: count the package-lifecycle module in llms.txt's schema inventory

check:llms-txt (the last step of the source-gates job) went red on the new
api/package-lifecycle.zod.ts: the hand-kept inventory declared 207 schemas
total and 29 under api. Re-read per the gate's own rule — number AND prose:
208 / 30, with Package Lifecycle added to the api row's key-schema sampler.
Gate re-derived green locally: 97 claims, 14 domains, 208 schemas.

Co-authored-by: Claude <noreply@anthropic.com>

* client: shrink the exported-any-returns ledger by the 15 gaps this PR closed

check:exported-any-returns (Type Check - consumer gates) is exact in both
directions: the objectstack-ai#11925 entries for the newly bound meta.* and packages.*
methods no longer resolve to any, so their ledger rows must be deleted.
meta.migrateStored's entry stays - it remains any by the 2C ruling. Verified
against the rebuilt dist: 'no NEW exported callable resolves to any: 317
callables reached, 47 ledgered site(s) still open'.

Co-authored-by: Claude <noreply@anthropic.com>

* spec: spell the discard-drafts capture's failure code as the ledger declares it

check:error-code-casing (Lint & Repo Gates) flagged the handwritten
conformance capture's code: 'item_locked' — an invented lowercase spelling.
The producer propagates deleteMetaItem's refusal code verbatim, and the
protection path sets 'ITEM_LOCKED' (ledger-declared, error-code-ledger.zod.ts
ADR-0010 §3.3), so the capture now carries the code the route really answers.
Gate and the conformance suite re-run green.

Co-authored-by: Claude <noreply@anthropic.com>

* merge origin/main (os-regen artifacts taken from main; regeneration follows)

* spec: regenerate the merged tree's artifacts (regen-merge sync after objectstack-ai#12849)

os-regen-merge.sh step 4: gen:migration-registry rebuilds registry.ts with
BOTH step-18 populations (objectstack-ai#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 objectstack-ai#4725/objectstack-ai#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>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

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