Repository navigation
feat(spec): form-view predicates loudly reject the features.* scope root - #12849
Conversation
…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
📓 Docs Drift CheckThis PR changes 1 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. 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 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
|
…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>
…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>
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 — andvisibleWhen'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): acrossapps/examples/content/over json/yml/yaml/mdx — positive controlvisibleWhen: 18 hits; targetfeatures.: 3 raw hits, all three the known false positives (two English prose sentences, one JavaScript string inside a code-preview widget). Zero authoredfeatures.*form-view predicates — the ruling's premise holds.Changes
packages/spec/src/ui/view.zod.ts—checkFormViewPredicateFeaturesRootscanner (CEL string literals stripped, thenfeaturesin root position; member access likerecord.features.xstays legal) wired intoFormViewSchema's existing superRefine, covering section-levelvisibleWhen, field-levelvisibleWhenat any nesting depth, and per-optionvisibleWhenauthored inline in the form view — bothsectionsand the legacygroupsbucket (pre-fold), the deprecatedvisibleOnalias spellings (refused post-fold), every formtypeincludingsplit, and the flattened runtime form overlay (refinement carried by.extend()). The refusal is a prescriptivecustomparse 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 existingpaneandsubmitBehavior.urlrefusals) and surfaces on the wire through the standard validation envelope — no new top-levelerror.codestring is introduced, so nothing enters the ADR-0112 ledger.18.ui-form-view-predicate-features-root-refused+gen:migration-registryregeneration.visibleWhenrows incontent/docs/ui/views.mdx;describe()strings updated (generatedcontent/docs/references/ui/view.mdxregenerated viagen:docs).@objectstack/specminor (accept-set narrowing, per the repo convention — same level as the FormField constraint-shape narrowing;check:changeset-no-majorrespected).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,visibleOnalias, bracket + bare-root + call-arg forms,type: 'split'), 5 positive controls (permitted roots,record.features.xmember access, longer identifiers, string literals, AST-only envelope's documented pass), 2 app-context pins (page component and bulk-action predicates namingfeatures.*still parse — that surface keepsfeatures.*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:blockedon 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)node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(7 changed paths vs merge-basef907fbe9e): 37 path-derived families + 6 convention-triggered (new-test-file kind). All measured families green, includingcheck: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, andcheck: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).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 flavorpnpm 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).pnpm --filter @objectstack/spec typecheckgreen (main + scripts + test layer; the new test file confirmed inside the tsconfig.test.json population viatsc --listFiles, 1 hit). Repo-widepnpm lint(eslint,--no-inline-config) exit 0 at the same HEAD.check:nul-bytesgreen (7141 files).features.invisibleWhen/visibleOnfixtures — the only hits are action-param fixtures (action.test.ts), an app-context surface this PR does not touch and pins untouched.grep -cof 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 viagit checkout HEAD -- path, proved by blob equality (git hash-object= HEAD blobbc2074d3…, marker count 0,git diff HEADempty); restored tree re-run: 16/16 green. No rebuild was needed for either leg — the suite imports./view.zodas 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