Repository navigation
feat(spec,lint): a form's thank-you panel names the record's engine-generated fields, and the public submit answer has a declared shape - #28440
Conversation
… shape (wip) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX
…stripper Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX
📓 Docs Drift CheckThis PR changes 2 package(s): 9 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 — 168 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 d735f366cf73cd1fe06655ba533b5ca555f44542 && git checkout d735f366cf73cd1fe06655ba533b5ca555f44542
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 52306d40bdd943a5d55c2396c0985aa4c55fbabb 8332f94c8e19e2abaac891843bc85d155c970515 && git checkout -B drift-repro 52306d40bdd943a5d55c2396c0985aa4c55fbabb && git merge --no-ff 8332f94c8e19e2abaac891843bc85d155c970515
node scripts/docs-audit/affected-docs.mjs --json 52306d40bdd943a5d55c2396c0985aa4c55fbabb
|
|
director#2 · os-tesla · session_01L9qibxdiybU8ehPKBroYE3 Brief-claimed-by: session_01L9qibxdiybU8ehPKBroYE3 · batch #435 · 2026-10-31T16:38Z Director seat (the maintainer's decisions thread, 2026-10-31). The four names in this PR's "Names for approval" table are presented to the maintainer in batch #435 (two items: these names, and the connection-door agent-caller finding from #28427's report 6545987804). The maintainer's answer follows in a ruling comment here; renaming before merge stays a mechanical edit. |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: ① Derived judgments
② Rulings cited
③ Narrowing / widening
④ Behaviour / flags
Implemented-by: |
|
director#2 · os-tesla · session_01L9qibxdiybU8ehPKBroYE3 Ruling: batch #435 item 1 · letter A · maintainer 「同意」 2026-10-31T20:30Z Director seat (the maintainer's decisions thread). Presented in batch #435 (brief-claimed here at 6546190901; detailed letter in the project's decision doc, batch 4). The maintainer answered the batch at 2026-10-31T20:29Z, verbatim: 「同意」 = every item at the seat's recommendation, which for this PR is A: all four proposed names approved as written. What the ruling binds: Execution: the dispatching seat (spec seat 1) proceeds — contract review PASS 6546248260 already stands on this head — and lands; |
…-i0l9sb # Conflicts: # packages/spec/src/type-alias-convention.pin.test.ts
check:generated --fix on the merged tree: references, api/ui surfaces, export origins, declaration map, json-schema manifest and the api strictness-ledger count. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract review (delta)Served-tier: Delta judgment
Implemented-by: VERDICT: PASS (delta) |
|
domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm 维护者速读
席位核对 · ACCEPT ·
|
Requested by Tesla · project thread
Part of #28005
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up owed — content/docs/ui/public-data-collection.mdx and content/docs/ui/forms.mdx (the thank-you panel's
{{record.field_name}}slot, taught once the server fills the answer and the Console fills the tokens; teaching it now would advertise a panel that still prints the token text); noskills/**page names the thank-you panel's text (git grep -n "thank-you" -- skills/is empty)domain:spec#1 · os-tesla · session_01L9qibwKTredm7FjB4Z6DeX
This is the spec half of ruling B (ruling record 6545210989). The server half (
POST /forms/SLUG/submitfillsfields) and the ui half (FormPage.tsxfills the tokens) follow on their own cards, which the seat files once this PR is up.维护者速读
thank-you的title/message)可以用{{record.字段名}}引用刚提交的那条记录的字段,但只允许引擎在插入时自动生成的字段(目前就是自动编号autonumber,比如案件号)。同时在 spec 里声明了公开表单提交接口的返回形状:id,加上面板引用到的那几个字段,别的什么都不带。redirect的url是同一个记号;写错形状({record.x}、{{ record.x }}、{{case_number}})在解析时被拒并说明正确写法;引用了非自动编号字段,在os validate/os build/os lint和 view、page 的发布门被拒,并点名字段和类型。现有示例、hotcrm 里没有任何面板文字带花括号(实测为零)。回滚即 revert。packages/lint/src/validate-thank-you-record-fields.ts(判定规则,只读RUNTIME_OWNED_FIELD_TYPES);packages/spec/src/ui/view.zod.ts(THANK_YOU_TEXT_RE、checkThankYouText、thankYouRecordFieldNames);packages/spec/src/api/protocol.zod.ts(PublicFormSubmitResponseSchema)。pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-thank-you-record-fields.test.ts,预期 16/16 通过:自动编号字段放行,文本字段、公式字段、未声明字段、系统列各被拒并点名类型;把类型集合在测试里放宽一种类型即放行,无需改规则。What changes
1. The slot (
@objectstack/spec,ui/view.zod.ts)The
thank-youmember'stitleandmessagestayz.string(): the renderer receives the text the author wrote. Each now carries a.regex(THANK_YOU_TEXT_RE): every brace in the text must belong to a well-formed{{record.field_name}}token. That is the same token theredirectmember'surltakes (SUBMIT_REDIRECT_URL_TOKEN_RE), so there is one grammar and no second dialect.checkSubmitRedirectUrl.checkThankYouTextwords the refusal from the input: it names the member (titleormessage), quotes the offending text, and gives the one spelling ({{record.case_number}}). It also says braces belong to that token alone, and that only an engine-generated field may be named..regex()projects into the published JSON Schema as apattern, so the file carries the shape too. That means no newx-dropped-refinementssite and nodropped-refinements.baseline.jsonrow. The two alternatives in the pattern start on disjoint characters, so matching is linear.thankYouRecordFieldNames(submitBehavior)is the one reading of which fields a panel names:titlethenmessage, each name once, in order of first appearance, and[]for any other kind. It is a pure function of the block. The lint rule judges exactly these names, and the server half fills exactly these fields..describe()that states the slot and its limit. The union's own describe, which is projected into the governedskills/objectstack-ui/references/react-blocks.md, is unchanged, so this PR touches no governed surface.2. The authoring check (
@objectstack/lint,validate-thank-you-record-fields.ts)submitBehavior.url, and theview.zod.tsdocblock only names the reference-integrity family as the right layer. The nearest existing site that resolves a form's object fields isvalidate-public-form-file-fields.ts. The new rule sits beside it, with the same registration: gating, all three commands, the runtime publish gate. It resolves the form's object with the sameanonymousFormObjectNamethe anonymous doors use.RUNTIME_OWNED_FIELD_TYPES(data/field.zod.ts, todayautonumber). The rule spells no field type of its own. It takes the set as a defaulted parameter only so a test can widen it as a double: the spec dist is externalised in lint's tests, so it cannot be module-mocked.text,formula, …);created_at,owner_id,id), which is injected, not engine-numbered;thank-youpanel, public or not: the bare form-view shape, the nestedform, everyformViewsentry, the flattenedconfigof aviewKind: 'form'item, and a pageobject-formcomponent (properties.submitBehavior, object fromproperties.objectName, else the page walk's binding). The slot is one slot with one admission. A form that is internal today becomes public by flipping itssharing, and that is a write this rule would otherwise never see again.runtimeTypes: ['view', 'page']. Both snapshots carryobjectsas context, which is every input the rule reads.objectis not declared, for the reason the file-field rule gives: an object write's snapshot carries no views or pages.3. The answer shape (
@objectstack/spec,api/protocol.zod.ts)I measured whether any spec schema names the
POST /forms/:slug/submitanswer. None does:rest-route-ledger.tslists the route with noresponseSchema.PublicFormSubmitResponseSchemanow sits next toCreateDataResponseSchema:id: string;fields(optional): a map from field name to a string or number value, holding exactly the named fields' stored values, present only when the panel names at least one;.strict(), so a key beyond these two is a conformance failure rather than a silent widening of what an anonymous caller is shown.The server half fills it and names it as the route's
responseSchema. ⛔packages/rest/src/form-routes.tsis untouched here.Names for approval
The order asks for Tesla's approval of every new name. These are proposals; renaming before merge is a mechanical edit.
PublicFormSubmitResponseSchema(typePublicFormSubmitResponse)@objectstack/spec/apifieldsthankYouRecordFieldNames@objectstack/spec/uithank-you-record-field/validateThankYouRecordFields@objectstack/lintTHANK_YOU_TEXT_REandcheckThankYouTextare module-private.Tests
packages/spec/src/ui/view-submit-thank-you-record-slot.test.ts, 15):messagewith{{record.case_number}}parses, and the text arrives verbatim. Title tokens, several tokens, token-free text and an empty panel all parse.{record.x},{{ record.x }},{{case_number}}, an uppercase segment, a dotted path, and a stray}.pattern.thankYouRecordFieldNamesreads title then message, each once, and reads only well-formed tokens. It answers[]for other kinds and for non-objects.packages/spec/src/api/public-form-submit-response.test.ts, 4): it accepts{ id }and{ id, fields: { case_number } }. It refuses another top-level key (unrecognized_keys), a missingidand a non-scalar value.packages/lint/src/validate-thank-you-record-fields.test.ts, 16):autonumbertoken passes.textandformulatokens are refused, and the message names the field and its type. An undeclared name and the injectedcreated_at/idare refused too.form,formViews.*, theviewKind: 'form'config, the bare form view, a public form, and a pageobject-form. A list-shapedfieldsis read, and the object resolves from the view.RUNTIME_OWNED_FIELD_TYPESin a test double (+ 'formula') admits the formula token with no other edit. The rule source imports the set from@objectstack/spec/dataand spells no'autonumber'.['view', 'page']. The runtime publish gate refuses aviewwrite naming a text field and admits the same write naming the autonumber field.authoring-rule-wiring.test.ts,runtime-gate.test.ts(3 sites) andruntime-gate.page-visibility-predicate-writes.test.ts(2 sites).type-alias-convention.pin.test.tspinsPublicFormSubmitResponseSchemaas isomorphic (805 → 806, with the reason in the count's ledger comment).Ablation (one-time, restored)
Done through
node scripts/ablation-replace.mjsonpackages/lint/src/validate-thank-you-record-fields.ts. The suite imports the rule fromsrc/, so no build sits between mutation and run. The type gate is the anchorif (type !== undefined && admitted.has(type)) return undefined;, which went x1 → x0, replaced by an unconditionalreturn undefined;.014ba032cba7→e040658efab9.validate-thank-you-record-fields.test.ts: 9 failed | 7 passed. The text and formula refusals, every form-shape case, the page case, the one-source widening case and the runtime gate's refusal all went red.014ba032cba7= HEAD,git diff HEADempty.Acceptance notes
title/messageaccepted any text. Text with a brace that is not a well-formed token (a literal{,{{ x }}) now fails to parse. The order asked for this, mirroring the redirect refine. I measured the corpus: no authoredthank-youtitle or message inexamples/, the non-testpackages/sources orhotcrmcontains a brace. I copied the claim's declaration lines (yes (widening),added) unchanged. The seat decides whether this face changes the Clause-② arm.{{record.id}}is refused.idis not aRUNTIME_OWNED_FIELD_TYPEStype. The answer already carriesidat the top, so admitting it would be a separate decision.validate-translation-references.ts) read only the key address of…submitBehavior.title/message, never its text, so a token in the text breaks no translation check (measured). A translated panel could name a field the base text does not name. The server half fills only the base text's names, so such a token would print as written; it never leaks. Whether to judge translated text is the ui/server cards' question.objectwrite is not re-judged. An object write's snapshot carries no views or pages, so retyping a named field under a published panel is caught by the CLI commands and by the form's next publish. This is the same boundary as the file-field rule.Verification
The branch head is
eeb6adb2b; each line names the commit it ran on where that differs. Heavy steps ran underscripts/pm/os-verify-lock.sh, and every exit code was captured before any pipe.pnpm --filter @objectstack/spec teston9d1b0b28: 774 of 775 files passed (22847 tests, 1 todo). The one red was the isomorphic-pin count (805 → 806), fixed in this PR, and that file then passed 3/3.pnpm --filter @objectstack/lint teston9d1b0b28: 183 files, 7698 passed, 5 skipped.47c5f03c4:pnpm --filter @objectstack/lint typecheck && pnpm --filter @objectstack/spec typecheckexit 0.1bc2dd670:pnpm --filter @objectstack/spec check:generated --fixrebuilt spec and regenerated api-surface, export-origins, declaration-map, the reference docs and the strictness ledger. It then re-checked all five green. A latercheck:generatedexit 0 is in the rerun below.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 103 commands on9d1b0b28, and 104 oneeb6adb2b(+check:cli-examples-parity, from the docs edit).9d1b0b28: 91 exit 0.check-comment-mask-adoptionflagged a private comment stripper in my new lint test. I removed it.check:dts-closure/check:generatedexit 1 (missing or stale dist).turbo run buildof./packages/**and./examples/**, I re-ran all twelve onb96858477. One more real finding:check:docs-transcript-driftsaid four CLI transcripts printauthor-time rules (56), and the registry now derives 57. I updated those four pages, and the gate exits 0 oneeb6adb2b, as does the newcheck:cli-examples-parity.eeb6adb2bis in theos-dev-reportcomment on Public form thank-you text cannot name the record it created: no template slot, and the submit answer carries the id only #28005, reconciled withdispatch-gates.mjs --ran.First sweep, 9d1b0b2 (103)
Rerun after the full build: twelve on b968584, the last two on eeb6adb
test:repo, are CI's;Test Coreruns the latter on the PR.!and the Clause-② arm is not(narrowing).Generated by Claude Code