Repository navigation
feat(spec,automation,app-shell): a read-only record_link screen field shows a record the run created, by its display name, as a link to its record page - #26355
Conversation
… defaultValue required, input keys refused Claude-Session: https://claude.ai/code/session_0115tbh4XfgivQD2RpHHwdMz Co-authored-by: Claude <noreply@anthropic.com>
…_link screen field; the flow dialog shows the record's name as a link Claude-Session: https://claude.ai/code/session_0115tbh4XfgivQD2RpHHwdMz Co-authored-by: Claude <noreply@anthropic.com>
…docs; add the changeset and the flows guide section Claude-Session: https://claude.ai/code/session_0115tbh4XfgivQD2RpHHwdMz Co-authored-by: Claude <noreply@anthropic.com>
…it created as record links, and a resume carrying a link's value is refused Claude-Session: https://claude.ai/code/session_0115tbh4XfgivQD2RpHHwdMz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115tbh4XfgivQD2RpHHwdMz Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 32 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 — 146 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 e2023d50b6eb0b87ce7e1919b5c992cd191c19dd && git checkout e2023d50b6eb0b87ce7e1919b5c992cd191c19dd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f60f4ea3ca7dd6b646014d02f52c2a9ae13e0820 a2c646635bd4b3517acf61d215c1f4a78b933a69 && git checkout -B drift-repro f60f4ea3ca7dd6b646014d02f52c2a9ae13e0820 && git merge --no-ff a2c646635bd4b3517acf61d215c1f4a78b933a69
node scripts/docs-audit/affected-docs.mjs --json f60f4ea3ca7dd6b646014d02f52c2a9ae13e0820
|
席位核对 · contract review + ACCEPT · seat
|
Requested by Steve · project thread
Fixes #26304
Fixes #25872
Clause-②: yes (widening)
Metadata-protocol: added
Summary
A screen flow that creates records can now end on a result screen that links them. This is ruling B on #25872 (6422899013) and the contract card #26304, landed in one PR across spec, engine and console, in the shape of the
filescreen field (#26134).ScreenFieldConfigSchemagains thetypemember'record_link'.referenceanddefaultValueare required on it, andrequired,options,placeholder,minandmaxare refused, each at its own key. Every othertypevalue parses exactly as before.validateScreenInputsrefuses any value sent under arecord_linkfield's name (nullincluded, visible or hidden) withinvalid_valueinside400 INVALID_SCREEN_INPUT. The value is never bound to a flow variable, and the refusal keeps the pause.judgeHeadlessScreennever counts arecord_linkfield as answered by a caller, so a result screen always pauses.ScreenFieldInputgets arecord_linkbranch. It shows the record's display name throughLookupField'sreadonlybranch (which fetches the record and names it from the referenced object's schema, ADR-0079nameField), inside a routerLinkto/apps/APP/OBJECT/record/IDbuilt byappRecordPath, the builder the record page's related lists use. An empty id draws the platform's "—" with no link. An object the console cannot route to, or a mount with no router, shows the name without a link.visibleWhenworks as on any field.initialScreenValuesnever seeds the field andscreenResumeValuesstrips it, so the dialog never posts it.content/docs/automation/flows.mdxgains "A read-only record link", and the resume-contract list names the new refusal. The designer form'sreferencecolumn now reads "Target object" and describes bothlookupandrecord_link.The member's name:
record_link, notrecordThe ruling let the dev propose the name and suggested
record. A screen field'stypeborrows the object field vocabulary (text,number,lookup,file, and so on), andFieldTypealready has'record'for a name-keyed map of embedded sub-objects (field.zod.ts, ADR-0007). One word with two meanings in one field vocabulary is a trap for an author, so the member isrecord_link, snake_case likemaster_detail. The name lives in one exported constant,SCREEN_FIELD_RECORD_LINK_TYPE, which the engine and the console both import.For the contract review: final shape and refusal texts
Shape: no new key.
typestaysz.string().optional(). The rule is a branch of the existingsuperRefineonScreenFieldConfigSchema, taken whentype === 'record_link'(exact match). It adds one issue per broken rule, each withcode: 'custom', and returns before the lookup rule..shapestill lists the same keys.referenceSCREEN_FIELD_RECORD_LINK_REFERENCE_REQUIRED: "Atype: 'record_link'screen field must declarereference— the name of the object the record belongs to (e.g.reference: 'crm_account'). Without it the field cannot name the record or link to its page."defaultValuenullSCREEN_FIELD_RECORD_LINK_DEFAULT_VALUE_REQUIRED: "Atype: 'record_link'screen field must declaredefaultValue— the id of the record it shows, usually a CEL value envelope over a flow variable (e.g.defaultValue: { dialect: 'cel', source: 'account_id' }). The field takes no input, so without it there is never a record to show."required,options,placeholder,min,maxrequired: falsetoo)screenFieldRecordLinkRefusedKey(KEY): "Atype: 'record_link'screen field shows a record and takes no input, so it cannot declareKEY: the run refuses any value submitted under its name. Remove the key, or use alookupfield to let the user pick a record."NAMEinvalid_value: "Screen field "NAME" shows a record and takes no value — leave it out of the resume", inside theINVALID_SCREEN_INPUTenvelope with the declared field listNew exports from
@objectstack/spec/automation(api-surface and export-origins regenerated):SCREEN_FIELD_RECORD_LINK_TYPE,SCREEN_FIELD_RECORD_LINK_REFERENCE_REQUIRED,SCREEN_FIELD_RECORD_LINK_DEFAULT_VALUE_REQUIRED,SCREEN_FIELD_RECORD_LINK_REFUSED_KEYS,screenFieldRecordLinkRefusedKey. TheScreenFieldSpecwire contract only gains docblock text. No newFieldErrorCodemember:invalid_valueis the catalog's "rejected for a reason no other member names".Tests
Measured on this branch.
@objectstack/spec:screen-field-record-link.test.ts(new, 9 tests) covers the accepted shapes (envelope id, literal id, help text,visibleWhen), each required key refused at its path, each refused key refused at its path (includingrequired: false), several broken rules reported together, the refusal throughScreenConfigSchema, other types untouched (includingrecord), and no tracker id in any refusal text. Full package: Test Files 805 passed (805), Tests 23263 passed, 1 todo.@objectstack/service-automation:screen-input-record-link-field.test.ts(new, 6 tests) covers no value accepted, any value refused (null, empty, the shown id, another id, an object,0,false), refused while hidden, exact-member matching, and the headless verdict with a positive control.builtin/screen-record-link-resume.test.ts(new, 2 tests) runs the engine: twocreate_recordnodes, then a result screen whose two links arrive typedrecord_linkwithreferenceand the created ids; a resume carrying a link's value is refused with nothing run after the screen; the clean resume completes. The account link is namedaccount, after the very variable its id comes from, so a bound refused value would have broken the audit write after the screen. Full package: Test Files 209 passed (209), Tests 2707 passed (2707).@object-ui/app-shell:FlowRunner.recordLink-25872.test.tsx(new, 6 tests) renders the realFlowRunnerand the realLookupFieldover a fake data source whose objects name records bycompany_name/full_name, notname. It covers two links with the right names and hrefs; "—" and no link for an empty id; an unroutable object and a router-less mount showing the name unlinked;visibleWhen; Submit posting only the real input; and the seed/strip helpers. With the neighboursviews/__tests__/FlowRunner*,metadata-admin/previews/andspec-symbol-parity: Test Files 160 passed (160), Tests 1794 passed (1794). The full app-shell suite is left to CITest UI.flow-screen-record-link.dogfood.test.ts(new) boots a real stack and drives the trigger and resume doors. The run creates an account and a contact and pauses on the result screen. Both fields arrive asrecord_linkwith their objects and the ids of the records this run wrote, and each id reads back through/data/OBJECT/IDwith itsnameFieldvalue (what the console shows). A resume carryingaccount_linkorcontact_link: nullis refused400 INVALID_SCREEN_INPUTnaming the field, and the clean resume completes the run.pnpm --filter @objectstack/dogfood exec vitest run test/flow-screen-record-link.dogfood.test.ts: Test Files 1 passed (1), Tests 2 passed (2). The full battery is not owed: no!in the title and no(narrowing)arm.@objectstack/spec,@objectstack/service-automationand@objectstack/dogfoodtypecheck, and@object-ui/app-shelltype-check, exit 0 each.pnpm exec turbo run build --filter=@object-ui/consoleon the merge heada2c646635b, exit 0.Ablation
Each mutation went through
scripts/ablation-replace.mjs, which restored the file to its HEAD blob withgit diff HEADempty.if (field.type === SCREEN_FIELD_RECORD_LINK_TYPE) {toif (false as boolean) {turned 5 of 9 tests red.record_linkresume pass (so it skips every field) turned 3 tests red across both new files.record_linkskip injudgeHeadlessScreenturned 1 test red. A first attempt used a replacement that was a substring of its anchor, and the tool refused it before any run; that attempt was void and was redone with a distinct replacement.if (isRecordLinkScreenField(field)) {toif (false as boolean) {inScreenFieldInputturned 5 of 6 tests red.initialScreenValuesturned 1 test red. The Submit test stayed green under that mutation becausescreenResumeValuesstill strips the key, which is the intended second layer.Gates
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat27c90bb39d(115 commands; the same list as at the earlier582dd2f9). Each ran with its exit code captured before any pipe, and--ranreconciled them: "115 derived famil(ies) accounted for — 114 run, 1 NOT-MEASURED". The 114 that ran exited 0. They includecheck-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check:api-surface,check:authorable-surface,check:export-origins,check:docs,check:doc-authoring,check:cross-package-test-inputs,check:test-source-alias,check-sdui-manifest(no record moves),check:nul-bytesandcheck:liveness.After merging
origin/main(12 commits, none touching this diff's files) the spec was rebuilt and the ratchet families re-ran on the merge heada2c646635b:check:generatedexit 0, andcheck:type-check-debtexit 0 ("0 raw tsc error(s) total, none above its recorded number").check:skill-examplesand thecheck-plugin-teardown-shapeself-test first stopped on missing prerequisites (an unbuiltclient-reactand a shallow clone); both re-ran after the prerequisite was met and exited 0.pnpm --dir ui run check:new-line-citations: 0 new citations.Not measured locally, left to CI:
check:dual-build-cjs-loads, which needs a full workspacepnpm buildand stopped on that prerequisite with exit 3 before reading anything. This diff does not change any package's build shape.Acceptance notes
typewas an open string, sotype: 'record_link'with any keys parsed and rendered a text input. It now refuses arecord_linkfield withoutreferenceordefaultValue, or with an input key. No producer of arecord_linkscreen field exists in this repository (git grepoverexamples/,packages/andui/source finds none), so nothing that parses today is refused. The claim'sClause-②: yes (widening)is kept.LookupField's readonly branch on "—", and the link around it still points at the record page, which then answers for itself. The name is never replaced with the raw id.record_linkexactly, as the spec and engine do, unlikefileandnumber, which the console lowercases. A field declaredtype: 'Record_Link'is an ordinary text input on every layer.record_linkscreen field, with an eval prompt (#26304 follow-up, #26184) #26347 (skills/objectstack-automation, an entry inevals/flows-triggers-approvals.json). This PR does not touchskills/**.@objectstack/*release carries this, the hotcrm seat can lift hotcrm#2285'sBlocked-by:.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_0115tbh4XfgivQD2RpHHwdMz