Repository navigation
feat(spec,service-automation)!: a screen's defaults, a field's defaultValue and recordId are value slots — a CEL envelope evaluated before the screen is served, the {…} token refused (#19939 pass 4, S2) - #22832
Conversation
… slots (#19939 S2) Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…e judge and the doors (#19939 S2) Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…ots before the screen goes on the wire (#19939 S2) Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…ped-refinement ledger (#19939 S2) Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
S2) Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 22 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 — 144 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 1721624a3871ce156e365eb2b97eed863e495323 && git checkout 1721624a3871ce156e365eb2b97eed863e495323
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a6f3a13f2aea4275e2984709461e7f4b1201e17e f915b87714d0d73d06248114fbefa1c8449fb08e && git checkout -B drift-repro a6f3a13f2aea4275e2984709461e7f4b1201e17e && git merge --no-ff f915b87714d0d73d06248114fbefa1c8449fb08e
node scripts/docs-audit/affected-docs.mjs --json a6f3a13f2aea4275e2984709461e7f4b1201e17e
|
Contract reviewServed-tier: Read: card #19939 body and all 39 comments (stage plan Check-runs on ① Derived judgments
② Semver level
③ Boundary flagsDev deviations (
Open question ( Out-of-scope carriers, each judged:
The patch round, one commit, re-reviewed on its head: (1) the Implemented-by: VERDICT: FAIL |
…; widening) — recordId newly accepts an envelope — and three sentences the screen value slots made false (#19939 S2, patch round 1) Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Delta review of patch round 1, against the FAIL record Read: the two commits since Check-runs on ① Derived judgments
② Semver level
③ Boundary flagsThe FAIL record's items, each closed at
Dev deviations (
The two pending changesets: leaving them to #22586 is RIGHT, and is the only route the repo allows this PR. Carriers from round 0, unchanged and still right: objectui#12101 (with the dev's restated precision on the preview: carries, marks, does not interpolate), S7 for the Implemented-by: VERDICT: PASS |
Part of #19939
Clause-②: yes (narrowing: a {…} token is refused at screen.defaults.*, a field's defaultValue and screen.recordId; widening: screen.recordId accepts a CEL value envelope it refused, and its published JSON Schema and inferred type gain the envelope arm)
Pass 4, stage S2 of #19939 (stage plan
6096263424, claim6107940231), under ruling D on #11182 and ADR-0032 Decisions 2 and 3. Ascreennode's three positions that hand a value to the client become value slots: an object form'sdefaults.*, a field'sdefaultValue, and an object form'srecordId. Each takes a CEL value envelope, which the executor evaluates in the run's scope before the screen goes on the wire, or a literal. A single-brace{…}token is refused at all three. #19939 stays open for stages S3 to S7, so this PR usesPart of.What lands
The contract and the ledger (
@objectstack/spec).ScreenConfigSchema.defaultstakesFlowValueSlotSchemaper key, and a field'sdefaultValuetakesFlowValueSlotSchema. Eachdescribe()is restated.ScreenConfigSchema.recordIdgains the envelope arm: a string, or a CEL value envelope (a union, so the published JSON Schema and the inferred type keep the string literal). A{token}in the string is refused by the one value-slot judge. Any other value (42,null, a list) is refused with a sentence naming what the id is for.FLOW_NODE_EXPRESSION_PATHSappends threevaluerows:screen.defaults.*,screen.fields[].defaultValue,screen.recordId.LEDGER_DECLARED_NODE_CONFIG_SCHEMAScarriesScreenConfigSchema, as it carriesMapConfigSchemasince S1, because the descriptor offers these positions with no marker. The ratchet finds the three markers on the spec Zod channel andfields[].visibleWhenon the descriptor channel alone.flowNodeValueTemplateRefusals) refuses a token at these positions atregisterFlowandobjectstack validate, and the envelope pass validates an envelope there. No second judge was added.dropped-refinements.baseline.jsondeclares the three new dropped refinements (defaults.valueType,recordId,defaultValue). The header total moves 705 to 708.The executor (
@objectstack/service-automation).screen-nodes.tsresolves the three readers through S1's value-slot resolver.defaultsgoes throughresolveValueSlotMap.defaultValueandrecordIdgo through the new single-valueresolveValueSlotinvalue-slot-map.ts, which the map resolver now calls per key, so the dispatch by shape is spelled once.AutomationEngine.evaluateValueEnvelopein the run's scope and context. A literal is served as written. There is no??fallback to the interpolator.recordIdmust evaluate to a non-blank string. Anything else fails the node there, naming its source; it is neverString(value).ADR-0087. The step-18 D3 entry
18.flow-value-slot-template-dialect-refusedis amended:replacementandreasonsay what each position serves where the template served nothing;acceptanceCriteriagains the screen check, and also the callee caveat the S1 ACCEPT carried to this stage.There is no D2: every whole-path spelling answers differently for an absent value.
registry.tsis generated at build from the entries and gitignored at this base (.gitignore:68), so no regenerated registry file is in the diff. The typecheck'sgen:migration-registryreports it current (422 semantic entries). The changeset marker isnot-required (already-registered flow-value-slot-template-dialect-refused), andcheck-adr-0087-registrationaccepts it.Sites migrated, re-derived at HEAD with the stage-0 census instrument. Positive control: the 3 tokened
screen.defaultsvalues and the 1 tokenedscreen.recordIdvalue it found at base read 0 tokened at HEAD.examples/app-crmcrm_convert_lead_wizard: 3defaultsvalues.companyis optional on a lead, so it is guarded.content/docs/automation/flows.mdx: the object-form example, the value-slot list, and a new paragraph on what each screen position serves.text-slot-template.test.tsrecordIdfixture, and test fixtures in spec and lint.Measured: what each position serves
Every reading is through the shipped
AutomationEngine, on the screen the paused run returns (screen-value-slots.test.ts).has(…) ? … : nulldefaults.*null, served as a key with valuenullfields[].defaultValuenullrecordIdnullis not a record idString(resolved): nothing, or[object Object]for a recordWhat the client does with it, read from objectui at the pinned
.objectui-sha20c6d351ad:initialRecord.tskeeps anullprefill member as a value, andseedCreateValuesputs the caller layer over the object schema's declared defaults. Sodefaults: { k: null }opens the field blank over the object field'sdefaultValue, which applied when the key was absent. The refusal and the D3 entry say: write that default in the guard.ScreenView.initialScreenValuesseeds a field from anydefaultValuethat is notundefined,nullincluded, andresumeWith(values)submits it.ObjectFormwithmode: 'edit'and norecordIdreads no record, and its save throws "Invalid form mode or missing record ID". This is whyrecordIdrefuses anullrather than serving such a form. The refusal tells the author to route around the screen with adecision.Premises checked against
f313fbcc99recordId. At base an envelope there was refused (z.string()): the parse refused it withinvalid_type, and the build doors withnode-config-refused-by-contract@recordId. It was not served verbatim. It held fordefaultsanddefaultValue. Measured by a probe throughflowNodeConfigRefusals,flowNodeValueTemplateRefusalsandScreenConfigSchema.ref()was at:234and used at:247,defaultswas at:250, anddefaultValueat:313.flow-node-config-refusals.ts. The build doors' value arm skipped every issue at or inside avalueslot. That would have stopped refusingrecordId: 42atFlowSchema.parse,registerFlowandobjectstack validate: a widening of the doors, because no other judge reads a value slot's literal type. It now skips avalueslot only where the slot holds an envelope (the envelope pass owns it); a token is skipped by the existing carve-out. Every other value slot's literal arm refuses nothing but a token, so no other verdict moves.insideValueSlotmatched only map paths. A malformed envelope atfields[0].defaultValuewas also reported asnode-config-key-required-by-ruleatfields[0].defaultValue.source. It now matches[]segments too.flow-builtin-node-config-values.test.ts), and ablations 4 and 5 below turn them red.schemaless-node-config.zod.ts:LEDGER_DECLARED_NODE_CONFIG_SCHEMASgainsscreen(the S1 precedent formap).value-slot-map.ts:resolveValueSlot. It is not exported from the package entry.Pins, and the ablations that turn them red
screen-value-slots.test.tsholds 25 engine pins:registerFlow, at the executor's guard and at the envelope pass, per position;dialecton the wire;nullper position;recordIdrefusingnull, a map, a list and a blank string.builtin-node-config.test.ts), ledger pins, screen-sentence pins (flow-value-slot-template.test.ts), the build doors' value arm, and the text-slot test re-judged in place.config-expression-ledger.test.tscovers the ratchet.lint-flow-patterns.test.ts.scripts/ablation-replace.mjs: the anchor hit once, the blob changed, and the restore was proven with blob equal to HEAD andgit diff HEADempty. Each was run at528f5ca363.resolveValueSlotnever evaluates (false && isExpressionEnvelopeShaped(value)): 11 of the 25 engine pins went red, the evaluation and absent/nullpins. The refusal pins stayed green, as expected.recordIdfalls back toString(id): the 5recordIdrefusal pins went red.screen.recordIdledger row renamed: 10 spec pins in 3 files went red.screen {"recordId":42} is refused at recordIdwent red.insideValueSlotback to map-only matching: the "slot another judge owns" control went red. It showed thenode-config-key-required-by-ruledouble report.@objectstack/spec'sdist. Ablations 3 and 6 turned the same functions red at source.Measurements
d693848a79.dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 115 commands from the 23 changed paths. All 115 exited 0.--ranwith the recorded exit codes reads 115 run, 0 NOT-MEASURED (a derived zero).check-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check:doc-authoring,check:i18n-coverage,check:examples-live-imports,check:nul-bytes,check:type-check-debt, and the spec gatescheck:generated,check:api-surface,check:export-origins,check:docs,check:spec-changes,check:upgrade-guide,check:authorable-surfaceandcheck:skill-examples.528f5ca363, three gates exited 3 (PREREQUISITE NOT MET):check:skill-examples,check:dual-build-cjs-loadsandcheck:i18n-coverage. They passed once the packages they read were built, and passed again in the full second run.check:future-spec-major, which exited 0. There is nocheck:migration-registryscript at this base.528f5ca363: 186 files, 2397 tests pass;d693848a79, after the second merge (which touched onlycore,mcpandmetadata-protocol): the 5 touched service-automation files (109 tests), specsrc/automation(1406) and the two touched lint files (227) pass.validateStackExpressionsreports 0 issues, the flow lint's value-slot hints 0, andregisterFlowacceptscrm_convert_lead_wizard. Its screens serve{"name":"Acme"}for a lead with a company and{"name":null}for one without.@objectstack/specand@objectstack/service-automationaremajor(pre modenext, re-read at push). TheClause-②line readsyes (narrowing: …; widening: …), on this body and in the changeset (patch round 1, after the contract review6109203893):{…}token is refused atscreen.defaults.*, a field'sdefaultValueandscreen.recordId;screen.recordIdaccepts a CEL value envelope thatz.string()refused, and its published JSON Schema (anyOf: [string, object]) and inferred type gain the arm:ScreenConfig['recordId']infersstring | AssignmentExpressionValuewhere it inferredstring.api-surface/**orexport-origins/**file moved.LEDGER_DECLARED_NODE_CONFIG_SCHEMASgainsscreen, which widensLedgerDeclaredNodeTypeandReconciledNodeConfigType; that is data in existing exports, as S1'smapwas.main(1330 changed lines).Acceptance notes
sourceenvelope atrecordIdreturns the envelope arm's own sentence, without theVALUE_ENVELOPE_REFUSALlead: the union reports that arm's issue directly.registerFlow's envelope pass, which runs first, leads with it.recordId: ''still parses, as it did. Only an evaluated id must be non-blank.recordIdastype: 'string'anddefaultsas an open key-value map, so the designer cannot author an envelope at these positions. Carrier: the objectui companion card v18: Studio's subflowinputand scriptinputsfields author a CEL value envelope, not a{var}token (the objectui half of objectstack-ai/objectstack#19939 pass 4, S1) objectui#12101, which is for the seat to extend. Sites there:flow-node-config.ts:856, whosedefaultshelp text names{account_id};previews/screen-spec.ts, which interpolatesdefaultValueanddefaultswith the single brace.validate-flow-template-pathsstill path-checks single-brace tokens at declared value slots, which doubles the judge's finding. This is the note passes 1 to 3 carried; S7 removes it.flows.mdxnamed asubflowinput as a single-brace position in two places, stale since S1. Both are fixed here, in the file this PR already edits: the surrounding examples are nownotifyrecipients andhttpbodies.skills/**carries no text about these three positions (measured, zero hits), so this stage owes no skills(objectstack-automation): SKILL.md still teaches{$User.<path>}as a kept value-slot spelling and the text-slot run-user remedy through it, which #19939 pass 2 (PR #22563) refuses #22585 pointer.Generated by Claude Code