spec: a flow screen field can express a numeric bound, help text and a lookup target - #17913
Conversation
…t and a lookup target `ScreenFieldConfigSchema` gains `min`/`max`, `inlineHelpText` and `reference` — names derived from `FieldSchema`'s own keys, so a flow screen field and an object field answer one vocabulary rather than two. The bound rides to the client on `ScreenFieldSpec` AND is re-checked on resume by `validateScreenInputs` (`min_value`/`max_value`, already in the ADR-0114 D2 catalog), because a screen field's declared contract is the only contract behind it. The executor forwards all four and the designer form offers all four, so the ledger test reconciles both key sets against the Zod. `reference` stays optional on `type: 'lookup'` — shipped flows declare a bare `lookup` screen field, and refusing them would break metadata that parses today. Also corrects the flows translation prose this change falsified: the screen field no longer "declares nothing help-shaped", so `help`'s exclusion from the bundle is restated as a not-yet rather than an absence. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
`automation-api.zod.test.ts` pins `TriggerFlowResponse['data']` EQUAL to the `AutomationResult` contract interface, so widening `ScreenFieldSpec` alone reddened it by name — which is the drift that pin exists to catch. The module-local `screenFieldSpecShape` now carries the same four keys, and the regenerated `builtin-node-config.mdx` reference picks them up. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…s four new keys `config-unknown-keys.test.ts` prints the declared set in its diagnostic and pinned the enumeration verbatim, so the four keys had to be acknowledged here. Pinned in full rather than sampled — a key arriving or vanishing on this surface should have to be stated. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
📓 Docs Drift CheckThis PR changes 2 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 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 3e2e52c54ac39f911e4c6bab7a5ad009258c4172 && git checkout 3e2e52c54ac39f911e4c6bab7a5ad009258c4172
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 45b90b6a99bbdbe344e6de766d8bee23b54b3c0f 68d0ce67114020e13201de9c03725b4eea219e37 && git checkout -B drift-repro 45b90b6a99bbdbe344e6de766d8bee23b54b3c0f && git merge --no-ff 68d0ce67114020e13201de9c03725b4eea219e37
node scripts/docs-audit/affected-docs.mjs --json 45b90b6a99bbdbe344e6de766d8bee23b54b3c0f
|
…d state the bound's real limit Three repairs from contract review, no behaviour change. 1. `FLOW_SCREEN_FIELD_NO_HELP` carried `#17306` in text printed AT the author, which `check:doc-authoring` refuses by design (no per-string exemption). The id is dropped from the string; the docblock above it still names the card for an internal reader. The test twin keeps pinning the new wording and gains the prescribed negative pin — the refusal must not match an issue id. 2. `packages/spec/liveness/translation.json`'s flows note was a fourth site still claiming the screen field "has no help-shaped key at all". Restated the way the three corrected sites are: the key exists (`ScreenFieldConfig.inlineHelpText`), what is missing is this face's translation key for it, and growing that face is a ruled step. 3. The bound was described as "enforced, not advisory" and as refusing any caller that skips the dialog. Measured, the resume check fires only on a value that is already a finite number, so a numeric string passes silently — the branch's own test pins that silence. The changeset, the flows doc, both `.describe()` pairs and the `ScreenFieldSpec` / `ScreenFieldConfigSchema` docblocks now state the numeric-value limit. Prose only: the loop is untouched. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…reen-field-bound-help-lookup
…ected bound wording `gen:docs` mirror of the two `.describe()` strings that now state the numeric-value limit. Generated, not hand-edited: the full run rewrote 222 files and only these two rows moved. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
… designer form too The Studio designer form's `min` / `max` column descriptions carried the same "Enforced when the run resumes." claim the review found false elsewhere, and they are read by the same author. Same correction, same measured limit, prose only — the form's key set and the Zod ledger that reconciles it are untouched. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…er gate
`check:reference-carrier-shape` refused (exit 3) on this branch: two
`reference` sites this PR introduced carry a non-string LITERAL at a
position its rules resolve under neither reading, so it declines to pick
one. Neither site is a relationship carrier, and none of the gate's three
site remedies is spellable at either:
- `screen-nodes.ts` — `reference` is a JSON-Schema property NAME in the
designer form's `properties` map. That holder is not under `fields:`,
its own `type` key holds a sub-schema rather than a string literal, and
all twelve of its keys are `data/Field` authorable keys, so no key can
prove it is not a field definition. The column moves to a named
constant; the emitted form is byte-identical.
- `builtin-node-config.test.ts` — the deliberate non-string in the
rejection pin. Giving that holder a FieldType `type` or a `fields:`
parent would turn a rejection fixture into a reported finding, and
`ScreenFieldConfig`'s twelve keys are all `data/Field` keys too. The
non-string values move to a named table, which also WIDENS the pin from
one shape to four — `{ object: 'x' }`, the carrier shape the gate exists
for, was not covered before — and each case now asserts the refusal
lands on `reference` with `invalid_type` rather than anywhere.
Both spellings sit in the population the gate documents as unjudged (it
judges literals), so they narrow nothing elsewhere in the tree. No path
ignore was added and the gate is not edited; its missing
`properties`-map rule is reported to the maintainer instead.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
… shape and require `reference` on a `lookup` screen field Maintainer ruling A′ (decision batch #130 item 1, 2026-09-13, verbatim 「同意」), applied across the five items it names. 1. Server-side enforcement at resume STAYS — `min_value` / `max_value`, both already in the ADR-0114 D2 catalog, no new error code. 2. The string gap closes. `validateScreenInputs` gains its own pass, BEFORE the bound: a present value for a `type: 'number'` screen field that is not a finite JSON number is refused with `invalid_type` — ⛔ never coerced. The bound pass compares numbers, so before this every non-number satisfied it by never reaching it (`"25"` under a `max` of 20 was conformant). The pin that recorded that silence is INVERTED in place, not deleted, so a later re-widening has to come back through it. Narrow in two directions on purpose: it keys off `type: 'number'` and not off the presence of a bound, and it is presence-conditioned exactly as the bound is. 3. `ScreenFieldConfigSchema` requires `reference` when `type` is `lookup`, via a `superRefine` that leaves `.shape` enumerable and the key set unmoved. This REVERSES the optionality the card first shipped; ADR-0078's own example of silently-inert metadata is a `lookup` with no `reference`, and a degraded shipped twin is not a reason to bend the contract to it. A stored bare lookup has NO lossless conversion — nothing in the metadata says which object the author meant — so it registers as an ADR-0087 SEMANTIC entry (`screen-field-lookup-reference-required`, protocol 18), ⛔ never a D2 conversion that would have to invent a target. The entry is one file under `migrations/entries/semantic/`; `registry.ts` is its GENERATED projection (`gen:migration-registry`), never hand-merged. 4. The wording items, in every carrier: the bound's "re-checked when the submitted value is a number" qualifier is gone from the changeset, the flows guide, the generated node-config reference, both `.describe()` pairs, the `ScreenFieldSpec` doc block and the Studio designer form, because the qualifier is no longer true. `reference`'s requirement is stated wherever its optionality was. 5. `check:reference-carrier-shape` is green (exit 0): both `reference` sites this PR introduced reach their value through a name, which is the population the gate documents as unjudged. ⛔ No path ignore, and the `['a']` fixture still tests what it tested — it widened to four shapes, including the `{ object: 'x' }` carrier shape the gate exists for. Also repaired, each falsified by the above rather than pre-existing: - The changeset declared no break. It now carries `**BREAKING**` and the `<!-- adr-0087: registered screen-field-lookup-reference-required -->` disposition marker — a semantic entry with no declaration on the changeset is exactly what `check-adr-0087-registration` exists to notice. `minor` stays: the launch-window guard keeps breaks off `major` outside pre-mode. - `ScreenInputIssue.code` enumerated `required` and `unknown_field` only and spoke of "the same two conditions". This PR put three more codes through that field. - `ScreenFieldSpec.reference` claimed absence was "what every `lookup` screen field did before this key existed". It now states that the authoring schema refuses that shape, and why the WIRE type stays optional: a run suspended before the upgrade rehydrates a `ScreenSpec` stored under the old accept set. - `api-surface/automation.json` and `export-origins/automation.json` were stale — the new exported refusal constant had never been propagated. Both regenerated with the repo's own generators; one additive line each. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Repair round complete — head
|
| command | exit | result |
|---|---|---|
@objectstack/spec test (local) |
0 | 474 files, 13497 tests |
@objectstack/spec test:repo |
0 | 31 files, 523 tests |
@objectstack/service-automation test |
0 | 133 files, 1574 tests |
both typecheck |
0 | — |
check:generated (15 artifacts) |
0 | was 1 before the fix |
check-adr-0087-registration · check:migration-registry · check:docs · check-doc-authoring · check-reference-carrier-shape (+ every --self-test) |
0 | — |
All suites went through os-verify-lock.sh (arrival depth 1), each wrapped in a retry loop inside a single Bash call — ⛔ no turn ended while waiting, which is how the previous round on this PR died (#17966). Exit codes from the wrapper's VERDICT line, ⛔ never a bare $? after a pipe.
check-reference-carrier-shape was run, not assumed from the commit message: --self-test exit 0, scan exit 0, "6685 file(s) scanned, 674 reference site(s), 0 non-string literal(s)", and re-run after the round's own edit to that file. No path ignore added; the gate is unedited; the ['a'] fixture widened to four shapes, never weakened.
⚠️ Open, and honestly stated
Item 4's "three wording items" could not be enumerated from the record. get_reviews returned [] and get_review_comments returned totalCount: 0; the only review text on the card (5650606835) names R1/R2/R3, all of which landed in d5843a793d6. Note-level items are in no artifact the round can read. It therefore judged wording completeness from the tree and verified by grep that no stale claim about this surface survives — with a lit control (inlineHelpText returns hits, so the zero is a reading). If any seat holds the list of three, check it against this head: the round may have covered them incidentally, or missed one that leaves no grep-able trace.
Two out-of-scope notes, neither filed: the entry file's explanatory comment run sits above the import type line rather than immediately above export const entry, so it does not reach the generated table (harmless — reason: carries the argument); and check-reference-carrier-shape still reports 44 unresolved + 12 conflicting sites tree-wide (0 material), unchanged — this PR routes around the gate's missing properties-map rule by naming its values rather than patching a gate from inside a feature PR.
Generated by Claude Code
Part of #17306
Clause-②: yes
ScreenFieldConfigSchemawas.strictover exactlyname/label/type/required/options/defaultValue/placeholder/visibleWhen, so three ordinary authoring intents had no expression at all — a numeric bound, help text, and a lookup target. They did not degrade quietly:max,helpTextand every lookup-target spelling were refused BY NAME. But a loud refusal with no landing key is still a dead end, and the reference app worked around all three in prose.Implements two director-seat rulings on this card, and the second reverses two choices the first head of this PR had made:
FieldSchema's own key names, and only together with their rendering.number-typed screen field that is not a JSON number is refused, ⛔ never coerced and ⛔ never silently passed; (3)referenceis REQUIRED ontype: 'lookup', carried by an ADR-0087 semantic migration entry.68d0ce67114. An earlier head of this same PR shipped the opposite on both points and pinned the opposite in tests; both assertions were inverted in place, with the reversal named at the assertion, so a later re-widening has to come back through them.The three spellings, and why each is the corresponding
FieldSchemakeyThe ruling requires names derived from
FieldSchema, never invented. Read frompackages/spec/src/data/field.zod.ts:min/maxFieldSchema.min/.max(field.zod.ts:1121-1122)FieldSchemadeclares exactly these two names for a numeric bound.inlineHelpTextFieldSchema.inlineHelpText(:1736)referenceFieldSchema.reference(:1213)FieldSchemarenamesrelatedTo/referenceTo/target/targetObject/lookupObjectonto it (:929). The card's proposedobject/reference_to/referenceToare all non-canonical.The one ambiguous case, and the test applied
FieldSchemadeclares two plausible help-shaped keys:description(:1001) — described as "Tooltip/Help text"inlineHelpText(:1736) — described as "Help text displayed below the field in forms"Test applied — the ruling's own "same spellings the object field uses": ask what
FieldSchemaanswers an author who reaches for this intent by its natural name. Its alias table (:921) readshelp: 'inlineHelpText', helpText: 'inlineHelpText', hint: 'inlineHelpText', tooltip: 'inlineHelpText'— so the object field itself routes all four natural spellings, including thehelpTextthis card asked for, ontoinlineHelpText.descriptionis never an alias target for them; it is a separate declared key for secondary/tooltip copy. ⇒inlineHelpTextis the spelling the object field uses for this intent.Reproduction of the gap (before the change)
Twelve spellings against
ScreenFieldConfigSchemaonorigin/main, withplaceholderas the lit control:The control could have come back the other way, and it is aimed at this schema's key set — which is what makes the twelve refusals a reading. The card's claim that
maxandhelpTextare refused by name is confirmed.What lands
Four keys (the bound pair counts as one intent, two keys):
min/max— forwarded ontoScreenFieldSpecfor the client and enforced server-side on resume byvalidateScreenInputs(min_value/max_value). A screen field's declared contract is the only contract behind it, so a bound the dialog alone applied would be bypassed by any caller posting toresumedirectly — the gap screen flows: resume performs no server-side validation — missing required fields and undeclared keys all complete the run #4477 closed forrequired. That guarantee holds for every submitted value, with no exception carved out for shape, because the value SHAPE is checked first and in its own pass: on atype: 'number'field a present value that is not a finite JSON number is refused withinvalid_type, ⛔ not coerced (ruling A′). All three codes —invalid_type,min_value,max_value— are existing ADR-0114 D2 catalog members: no new error code.inlineHelpText— help under the input, distinct fromplaceholder, which the browser clears the moment the user types. That is precisely the carrier the reference app was forced to overload for a constraint that has to stay readable.reference— the lookup target, so the field can resolve a record picker. Required whentypeislookup, as it is on an object field (field.zod.ts:1196-1199: alookup/master_detailwhosereferenceis missing, empty or whitespace-only is refused at parse time). Optional in the shape, required by asuperRefine, so the key set does not move and.shapestays enumerable.Delivered with its rendering inside this repo. The executor forwards all four and the Studio designer form offers all four as repeater columns;
builtin-node-form-zod-ledger.test.tsreconciles the two key sets against the Zod in both directions, so a key declared here and absent from the form fails that test rather than shipping as a field nobody can author. The console dialog half is objectui#9248, which this card's acceptance includes.What this release BREAKS
⛔ This is not a purely additive release. The changeset declares BREAKING in two places and grades
minoron both packages —minorbecause the launch-window guard (check-changeset-no-major) keeps breaking changes offmajoroutside pre-mode, not because the narrowing is small. Under strict semver it would be major, and the changeset says so in its own words. Precedent:schedule-flow-acting-organization-required(323da7a30d0).Break 1 — a stored bare
lookupscreen field parsed before and is refused now.ScreenFieldConfigSchema.superRefinerefusestype: 'lookup'with an absent or blankreference, addressed to['reference'], with the exportedSCREEN_FIELD_LOOKUP_REFERENCE_REQUIREDas its message. A picker with no target object resolves nothing — ADR-0078's own worked example of silently-inert metadata — and ruling A′ held that a degraded shape which already ships is not a reason to bend the contract to it. The reference app's own hotcrmclose_case"Resolved by Article" field is exactly such a site; it namescrm_knowledge_articlewhen it adopts this version (relayed to the hotcrm seat; the director seat has no write access there).There is no lossless conversion — nothing in the stored metadata says which object the author meant — so this is an ADR-0087 semantic entry (
screen-field-lookup-reference-required, step 18, the unreleased major), a structured TODO naming the flow and the field for a human to answer, and ⛔ never a D2 conversion that would have to invent a target. The changeset carries the disposition marker andregistry.tsis regenerated by the repo's own generator, byte-identical.Break 2 — a resume bag accepted before can be refused now. A non-number submitted for a
type: 'number'screen field is refused on resume withinvalid_typeinstead of passing silently. Before this, the bound pass compared numbers, so a value that never reached it satisfied it: under amaxof20the string"25"was conformant. It was never doing what its author declared.Everything else is additive. The other three keys widen the accept set for every field; the bound itself fires only on a field that declares one, which nothing did before this release; and a
lookupfield that already names its target parses byte-identically.ScreenFieldSpec.referencestays optional on purpose: a run suspended at a screen before the upgrade rehydrates itsScreenSpecfrom context stored under the old accept set. Such a client has no target to resolve a picker against and falls back to a plain input — the pre-#17306 behaviour, kept reachable rather than turned into a client-side crash. Drain or re-drive those runs rather than assuming the fix reaches them retroactively; the migration entry's acceptance criteria says so too.Both-direction pins
Accepted: all four keys; the two hotcrm acceptance fixtures as declared metadata (
quote_generation's discount ceiling,close_case's lookup with its target); a field with noreferenceon every othertype— the requirement reads one member of an open widget vocabulary, soundefined/text/number/selectare untouched.Still refused: a
lookupwith an absent, empty or whitespace-onlyreference, addressed toreferenceand carrying the exported message (which is also pinned to name the key, show the spelling and carry no tracker id); an undeclared key by name (sparkles) — a.strictschema that quietly widened past these four is the regression that catches; a non-number bound (max: '20'→invalid_type); a non-stringinlineHelpText; and a non-stringreferenceacross four shapes (['a'],{ object: 'crm_account' },42,true), each required to fail onreferencewithinvalid_typerather than incidentally. Plus an exact key-set pin enumerating all twelve declared keys, which is the only thing that can see a fifth key arriving, and a pin that.shapestays enumerable —superRefineis a CHECK, not a wrapper, and aZodEffectshere would break the ledger test silently by making the key set unreadable rather than wrong.Refusal quality:⚠️
help/helpText/hint/tooltipare refused naminginlineHelpText, andobject/referenceTo/targetObject/lookupObject/relatedTo/targetnamingreference.objectmeans different things one level apart — on the screen node it renames toobjectName, on a screen field it can only mean the lookup target — and a pin holds both readings apart.Ablation — measured on the pre-A′ tree, ⛔ NOT re-run at this head
Two legs on
screen-input-contract.ts, each proven on disk by occurrence count andgit hash-object, with atraprestore:maxcheckmax"; restored (hash1cb0df1…== HEAD blob,git diff HEAD0 paths) → 8/8 GREENLeg B is the cost direction: it proves the pin that guarantees the bound does not start refusing values the user was never asked for.
68d0ce67114it carries 10: A′ added the value-domain pin and the shape/requiredsplit, and inverted the pin that used to assert the silence ("does not fire on a non-numeric value" → "refuses a non-number for atype: 'number'field — the string gap is closed"). The8/8totals above therefore describe a superseded tree. NOT MEASURED at this head: the two legs have not been re-run against the 10-pin file, and the A′ guards (the shape pass, thelookuprefinement) have no ablation of their own. Their pins pass at head and in CI; that is coverage, not an ablation reading.One stale claim corrected, because this change falsified it
The flows translation surface documented
help's exclusion as "ScreenFieldConfigdeclares nothing help-shaped at all" — intranslation.zod.ts's guidance string (which enumerated the old key set verbatim), its doc block,i18n-resolver.ts'sFLOW_SCREEN_FIELD_COPY_KEYS, andpackages/spec/liveness/translation.json. The screen field now declaresinlineHelpText, so the copy is real. The exclusion stands — the flows bundle still carrieslabel/placeholderonly, and growing that face is a ruled step against the #7646 enumeration, not a resolver-side accretion — but its reason is now stated as a not-yet instead of telling an author the field has no help copy when it has. ⛔ No translation key added, no resolver behaviour moved.A pin caught a real divergence mid-round
automation-api.zod.test.tsbindsTriggerFlowResponse['data']equal to theAutomationResultcontract interface. WideningScreenFieldSpecalone reddened it by name (TS2344) — exactly the two-files-one-shape drift that pin exists to catch. The module-localscreenFieldSpecShapenow carries the same four keys.Generated artifacts
check:generatedis 0 at this head (it was 1 before the repair round below found it). The generated delta against the merge base:authorable-surface/automation.jsoninlineHelpText,max,min,referenceonautomation/ScreenFieldConfig, and nothing elseapi-surface/automation.jsonSCREEN_FIELD_LOOKUP_REFERENCE_REQUIRED (const)export-origins/automation.jsonsrc/automation/builtin-node-config.zod.tssrc/migrations/registry.tscontent/docs/references/automation/builtin-node-config.mdxRegistry wiring was verified generated, ⛔ not hand-typed:
registry.tscopied aside,gen:migration-registryre-run (exit 0),diffexit 0 — byte-identical.Round history on this PR
e32ab94fb9b…6db3438f26717db348fc3dFLOW_SCREEN_FIELD_NO_HELP(translation.zod.ts) carried an internal issue id inside text printed at the author;Doc/skill authoring guardrefuses it with no per-string exemption by design — this was the CI red. The id was dropped from the string; the doc block above it still names the card for an internal reader, which the guard permits. Thetranslation.test.tstwin keeps pinning the new wording and gained the prescribed negative pin — the refusal must not match an issue id.17db348fc3dpackages/spec/liveness/translation.jsonwas a fourth site still claiming the screen field "has no help-shaped key at all". Verified hand-kept (onlystate-counts.mdis generated underliveness/). Restated the way the three corrected sites are.d5843a793d6c6ea7ea54a3referencesites legible tocheck-reference-carrier-shape(see below).68d0ce67114referencerequired onlookup; added the ADR-0087 semantic entry, the BREAKING declaration and the disposition marker; inverted the two pins that recorded the old behaviour.typeof value === 'number' && isFinite, called "a caller that skips the dialog is refused too" an overclaim, and edited six carriers — the changeset,content/docs/automation/flows.mdx, both.describe()pairs, theScreenFieldSpecdoc block, theScreenFieldConfigSchemabound doc block, and the Studio designer form'smin/maxcolumn descriptions — to state the numeric-value limit, leaving the loop untouched. Ruling A′ closed the gap in code instead. At this head those carriers state the guarantee unqualified again, and the designer columns read "Enforced when the run resumes." with no "for a submitted value that is a number" clause. The loop is not untouched: a value-shape pass now runs before the bound pass.R1 reproduction, before and after
(The
aftercounts are the reading at this head; the R1 round measured 15160 / 948 on its own, smaller tree.)The second red is closed
check-reference-carrier-shapewas RED atd5843a793d6(exit 3, "could not measure") on two sites this PR introduced: the designer form'sreferencecolumn definition (screen-nodes.ts) and the deliberate non-stringreferencefixture inbuiltin-node-config.test.ts. Ruling A′ item 4 dispatched it: test the gate's third remedy first, ⛔ never change the['a']fixture to silence the gate, ⛔ never a path ignore.c6ea7ea54a3did that. The third remedy is not satisfiable at the designer-form site — the holder is a JSON-Schemapropertiesmap whose owntypekey holds a sub-schema, and all twelve of its keys aredata/Fieldauthorable keys, so no key can prove it is not a field definition. Both values are therefore reached through a name (LOOKUP_TARGET_COLUMN,NON_STRING_LOOKUP_TARGETS), which the gate documents as unjudged because it judges literals only. The gate is unedited, no path ignore was added, and the fixture was widened from one shape to four with a stricter assertion, never weakened. It exits 0 at this head.⛔ Not fixed here, for the seat to dedupe:
check-reference-carrier-shapehas no holder rule for a JSON-Schemapropertiesmap, and its literal-only predicate leaves any named value unjudged. Patching a gate from inside a feature PR is the wrong place for it.Verification at head
68d0ce67114d5843a793d6, two commits back.CI on this head (35 check-runs): 33 success, 2 skipped, 0 failure. The two skips are
Console Pin Gate(path-skipped —.objectui-shaandbuild-console.share untouched) andPacked-tarball smoke (opt-in).Lint & Repo Gates,TypeScript Type Check(the job carrying the eight generated-artifact gates) andBuild Coreare all green.Implementing seat, locally on this head:
pnpm --filter @objectstack/spec test(--project local)pnpm --filter @objectstack/spec test:repo(--project repo)pnpm --filter @objectstack/service-automation testtypecheckcheck:generated(15 artifacts)check-adr-0087-registration·check:migration-registry·check:docs·check-doc-authoring·check-reference-carrier-shape(+ every--self-test)At-tier contract review, independently on this head (comment 5652925641, detached worktree, fresh install, 21/21 build closure):
check-adr-0087-registration --base origin/mainexit 0 — 1 declared-breaking changeset,registered screen-field-lookup-reference-required, so the gate now has an input where the earlier green was vacuous;check-changeset-no-majorexit 0 with the LEVEL AXIS clean;check-empty-changesetexit 0;check-reference-carrier-shapeexit 0 (6685 files, 674referencesites, 0 non-string literals);check-doc-authoringexit 0 (15163 strings / 949 sources); the three suites andservice-automation typecheckexit 0, with the builtdist/automation/index.d.tscarryinginlineHelpTextas the positive control.NOT MEASURED (neither a pass nor a finding): the ablation legs at this head (above);
@objectstack/spec typecheckin the review's container (SIGTERM); and five derived gate commands that exit non-zero for checker-health or unbuilt-prerequisite reasons rather than on this diff —check:dual-build-cjs-loads,check:i18n-walk-parity,check:skill-examples(allPREREQUISITE NOT METon unbuilt output outside this change's dependency closure),check:role-word(its own--self-testfails in that container),check:pm-prior-rulings(script absent at this head). All of them live in jobs that build first, and CI is green on this head.维护者速读(草稿)
改了什么 — 流程界面的输入框,现在能表达三件以前完全没法表达的事:数字上下限、一句帮助说明、以及查找框要从哪个对象里选记录。三个键的名字全部沿用对象字段已有的拼法(
min/max、inlineHelpText、reference),没有发明新名字。同时收紧了两处旧行为:查找框必须写明目标对象;数字字段在流程恢复时提交的值必须真的是数字。为什么改 — 以前作者想写这三件事,只会被拒收,而且没有任何可用的替代键。参考应用只能把折扣上限写进标签和占位符里,把查找框降级成"请人工输入记录 id"。也就是说,我们自己的样板应用在用变通手段绕过我们自己的契约。至于那两处收紧:上下限如果只有对话框在管,绕过对话框直接提交就能突破,等于那条规则形同虚设;而没有目标对象的查找框,渲染出来就是一个查不到任何记录的选择器 —— 这正是 ADR-0078 拿来举例的"解析得过、却什么都不做"的元数据。
风险与代价(含回滚) — ⛔ 这是一次破坏性变更 —— 不是只往外加键。 changeset 两处写明 BREAKING,版本级别按发布窗口惯例记为
minor(门禁在非 pre 模式下不允许major),按严格语义化版本它应当是 major,changeset 里也是这么写的。具体到作者会遇到什么:
reference的lookup界面字段,从这个版本起解析会直接失败,报错指向reference这个键并给出正确写法。⛔ 没有任何自动迁移能修它 —— 元数据里根本没有记录作者当初想指向哪个对象,所以升级链里放的是一条结构化 TODO(点名是哪个流程、哪个字段),由人来逐条回答。我们自己的参考应用 hotcrm 的close_case就是这样一处,它在采用这个版本时会补上crm_knowledge_article。"25"这种数字字符串会被拒收,以前是静默放过。⛔ 不做隐式转换 —— 把"25"偷偷读成25会让上下限的判定依赖一个契约里没声明过的转换。以前能被接受的提交,现在可能被拒。回滚代价:三个新键一旦发布,删键很贵;两处收紧则可以单独放宽(两条对应的测试都是就地反转而不是删除,所以任何一次回头放宽,都必须从那两条断言前面经过,不会悄悄发生)。
席位意见 — (待席位填写)
你要做的(一个动作) — 审阅这次公开面的扩宽与两处收紧是否如你所愿。⚠️ 请特别确认第 1 条的代价你能接受:已上线的流程如果有没写目标对象的查找框,升级后会解析失败,且只能人工逐条修。另外,objectui#9248(console 对话框渲染这三个键)落地之前,本卡不应关闭 —— PR 正文用的是
Part of,不是Fixes。Generated by Claude Code