feat(spec)!: object-gantt markers, object-timeline mapping and both forms' fields take the shape each block reads; five objectui-held contracts reported as forks (#21464, S-objectui-held) - #21699
Conversation
…d form fields typed; forks recorded Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude <noreply@anthropic.com>
…ness counts Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude <noreply@anthropic.com>
… audit reaches its table Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check11 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 138 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 0b902788dd7ccc2935c5468004c5db734a782c64 && git checkout 0b902788dd7ccc2935c5468004c5db734a782c64
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 83e2feeb46be3c22b7139b01114612e93afcf412 723df5474159ed4b2100cd3742a6874f7e22fd69 && git checkout -B drift-repro 83e2feeb46be3c22b7139b01114612e93afcf412 && git merge --no-ff 723df5474159ed4b2100cd3742a6874f7e22fd69
node scripts/docs-audit/affected-docs.mjs --json 83e2feeb46be3c22b7139b01114612e93afcf412 |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 37185775211 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Part of #21464
Clause-②: yes (narrowing)
What this does
The S-objectui-held stage of the
ComponentPropsMapz.unknown()close-out, per the claim5976829576, the stage-5 landing record5976140848, the seat answers5963787404and5969669440, the stage-5 ACCEPT5975923386anddomain:devx's pointer5974144504. The stage took its own value census first (below), then wrote down and typed the contracts that have one viable shape. Under the stop valve, it holds the five contracts (seven ledger lines) that have more than one shape, records them in the enumeration pin, and reports them on the card as forks. Read points are at the.objectui-shapinab1879721595. Every cited reader is byte-identical at objectuimain94985a92ba; the two cited@object-ui/typesdeclaration files changed elsewhere, and their cited lines are identical.ab1879721595object-ganttmarkersz.array(z.unknown()){ date, label?, color? }entries,datea string (module-private factoryobjectGanttMarker()), with aliasestitle/text/nametolabelandcolour/stroketocolorObjectGantt.tsx:2505handsschema.markerstoGanttView, which re-basesdate(GanttView.tsx:913-924), drops a marker whose date does not parse or falls outside the range (:2394-2413), and drawslabelandcolor(:2407,:4083-4100, SVG:3320-3332). objectui declares the authored marker as exactly this shape (types/src/objectql.ts:3787-3794, zod mirrortypes/src/zod/objectql.zod.ts:2738-2746)object-timelinemappingz.unknown(){ title?, date?, description?, variant? }, each a field name (module-privateObjectTimelineMappingSchema), with aliases from the flattitleField/dateField/startDateField/descriptionField/variantFieldObjectTimeline.tsx:551(title),:576(date),:578(description),:579(variant). objectui declares the same four strings on the component prop (:254-259) and inTimelineMappingSchema(:144-149)object-formfieldsz.array(z.unknown()), held at stage 3formFieldNameList()). A{ name }or{ field }object entry is refused with what to write insteadObjectForm.tsx:961-981,flatFields.ts:68-79. objectui declaresObjectFormSchema.fields: string[]. The form still draws a STORED{ name }entry by its name (:972,flatFields.ts:72) and skips any other object entry with a warning (:978)object-master-detail-formfieldsz.array(z.unknown()), held at stage 3MasterDetailForm.tsx:1693The
fieldshold's exit. objectstack-ai/objectui#11550's triage ruling (5969880008there) retired the{ name }entry from objectui's authoring faces, and its closing paragraph says this card "types object-form and master-detailfieldsas names once its.objectui-shacovers this landing". The pin covers both code landings: objectui806f327(the fixtures respelled) anddbd1081(the registration descriptions and the warning text).merge-base --is-ancestorexits 0 for each. The guide's own respelling (0a53c67) is on objectuimainonly. Measured at both trees, the only refused values are fixtures probing the read (census below).No new export. The three new shapes are module-private. No new member carries a default or a transform, so each parsed value is the authored one. The gantt marker is a factory the row calls, as
masterDetailDetailEntry()is, and not alazySchemaproxy. A bare proxy used as an array element is never forced byalias-integrity.test.ts's walk, which skips def values that are functions. The first full-suite run caught exactly that:alias integrity — coveragelistedui/component.zod.ts:6611 (this object-gantt marker)as unreached. The factory form is green.Held as forks (the stop valve)
Each of these has two or more viable spec shapes that no existing ruling or seat answer decides. Each keeps its hold. The enumeration pin's ledger now files each under a new
forkstage that names the shapes, and §2 checks that everyforkline names at least two different shapes. The emptiedobjectui-heldstage leavesSTAGES. Each fork is reported in this stage'sos-dev-reportopen_questionswith its census.object-metricdrillDown.report: direction A's premise is disproved by measurement, so it is reported as a fork and takes no shape, as the ACCEPT5975923386directs. Premise (1) holds: the one drawn report the census finds (objectMetricDrillDownMembers-8071.test.tsx:282, a dataset-bound summary report withnameandlabel) parses throughReportSchema. Premise (2), thatisDatasetBoundReport's two arms are exactly whatReportSchemaadmits, fails in both directions (measured by parse, this branch's source):isDatasetBoundReport)ReportSchema{ name, label, type: 'joined', blocks: [{ name }] }, no block binds a dataset{ dataset: 'sales' }, noname/label/values{ name, label, dataset: 'sales' }, novaluescustomatdataset){ name, label, type: 'joined', dataset, blocks: [...] }customatdataset)The first row is the silent-fallback class this card closes. It comes from
ReportSchemaitself: a joined block'sdatasetis optional, although the schema's own refinement comment says each block is "dataset-bound".objectstack validatepasses such a report with exit 0, while a bound block naming an undeclared dataset is refused (chart-dataset-unknown). See Acceptance notes.object-formcustomFields: objectui's runtimeFormField(types/src/form.ts:1770) is open (an index signature beside forty-five members,:1906), and eight of its members are the grid widget's snake_case keys (min_rows,allow_add, …), which this package's camelCase rule for config keys does not admit as written.object-formandobject-master-detail-formsections: objectui#11550 KEPT the inline runtime field in a section ("shape 3",sectionFields.ts:369-370, declaredObjectFormSection.fields: (string | FormField)[]), so this waits oncustomFields's fork for its third entry arm.object-timelineitems: a feed entry'scontentis child schema nodes (a slot position the page walks would judge, or an opaque member), and the arm an entry must match is chosen by the parent'svariant(objectui's row-level refinement, or a plain union of the arms).action:group/action:menumembers: measured from the reads, the key set is mostlyaction:button's, keyed bytype. It also takes keys the rows leave undecided:outcomeMessages(recorded onaction:buttonas "a contract decision, not a pin re-measure"), a memberclassName, the member's ownproperties.paramsbag (static-params.ts:142-160) andendpoint(refused on the rows since finding(spec):action:button/action:iconacceptendpoint, the keyActionSchemarefuses with the prescriptionendpoint→target: one concept, two verdicts in one release #21005).The
object-kanbanconditionalFormattinghold (held-for-decision) is not in this stage's member list and is untouched. Its carrier, objectstack-ai/objectui#11522, is now in statecompleted(2026-10-03), so its exit may be readable. That is for the seat.The census
A writer is a value written on the block: a page-component node (an object literal naming the type, flat or in
properties, a literal annotated or asserted with the block's objectui type, a direct parse through the row), the block's React component with the member as a prop or insideschema={{…}}, or the argument of a same-file helper that mounts one (helpers found through a block literal or the block's JSX in their body, including one spread from a const; positional parameters resolved at every call site). Values resolve through same-file constants and spreads. Instrument: a TypeScript-AST walk over.ts.tsx.js.jsx.mjs.cjs.mts.json.yaml.md.mdx(fenced code parsed). For the typed members, a text search for the member key in every file naming the block found what the walk does not reach, and each hit was read by hand. Cross-check: the walk reproduces stage 5'sdrillDownpopulation exactly (26, 25 static).markersmappingfields, both forms7d0781482d(examples/,packages/,content/,skills/,apps/,docs/, 9306 files)ab1879721595(whole tree, 10267 files)main94985a92ba(7564 files; the release deleted 2726 changesets)4054ec2680, cloudb2d7a7f6f8objectstack writers:
packages/spec/src/ui/component-element-navigation-17987.test.ts(mapping: { title, variant }). For master-detailfields: the showcase'sproject-workspace.page.ts,content/docs/protocol/objectui/layout-dsl.mdx, and two test copies (packages/lint/src/validate-component-props.test.ts,packages/spec/src/ui/component.test.ts). All are field names.objectui refused values, under the writer test (
5966636964):markers: [{ date: 5 }]types/src/__tests__/gantt-declared-keys.test.ts:168@ts-expect-error)fields: [{ field: 'note' }],[{ field: 'sent_at' }]plugin-form/src/__tests__/objectFormFieldsMembers-8071.test.tsx:140,:199fields: [{ name: 'note' }]×2objectFormFieldsMembers-8071.test.tsx:177,:182fields: [{ field: 'note' }]topLevelFieldsWarnCoverage-8847.test.tsx:110fields: [{ name: 'note' }, 'status']topLevelFieldsWarnCoverage-8847.test.tsx:258fields: [{ name, label, type, required }, …](pin only)skills/objectui/guides/page-builder.mdmainNot writers: five
markersarrays mountGanttView, the runtime chart, directly (GanttView.virtual.test.tsx×2,GanttView.dateOnlyZone-10866.test.tsx, the plugin README'sGanttViewexample,demo/main.tsx). Fourteenfieldsmatches are object definitions or permission maps whose ownfieldskey the walk read as the block's (ObjectForm.effectiveOps×3,ObjectForm.managedEdit×3,ObjectForm.mobileFullscreen×3,drawerFormSectionDescription-9834,objectFormNumericStep-9574×2,LineItemsPanel.fieldWriteGate-10163×2). The 11 that are not static are run-time hand-offs (AppContent,useActionModal,RecordFormPage,ViewPreview,StudioDesignSurface,DrawerForm,EmbeddableForm,ModalForm,ObjectForm.tsx:387,MasterDetailForm.tsx:1693,ObjectView). None passes through the component-props gate.The forks' census, both objectui trees identical. objectstack, hotcrm and cloud author none of these, except one
itemsand threesections(strings) in objectstack.drillDown.report: 3 values. The one drawn report parses throughReportSchema; the twoit.eachprobes ({ note }, the retiredobjectNameform) are not drawn and are refused.customFields: 27 values (24 static, 31 entries), every entry keyednamewithlabelandtype. One carriesgroupand onedefaultValue, keys outsideFormField's forty-five that ride its index signature. No entry writes a snake_case grid key.sections: object-form 165 values (99 static); 192 string entries, 3{ field }entries and 7 shape-3 inline runtime fields (plugin-form/README.md's wizard,submitTargetRefusal.test.tsx×3). Master-detail: 13 values, strings only.items: 14 values (11 static): 8 feed entries and 5 gantt rows, none withcontent.action:group/action:menu: 40 / 19 values (12 / 6 static, 15 / 6 members). The keys used arename,label,type,locations,target,bodyExtra,bodyShape,objectName, and oneautoTriggerin a host auto-trigger test. None uses an undecided key.Changes
packages/spec/src/ui/component.zod.ts:objectGanttMarker()(with the reason it is a factory),ObjectTimelineMappingSchema,formFieldNameRefusalandformFieldNameList()with their docblocks. The four members are typed. The held members' docblocks now record each fork, re-read atab1879721595: the drillreportwith the measured disagreement,customFields, bothsections,itemsandactionMemberList. The gantt and timeline row docblocks no longer saymarkers/mappingstay open.packages/spec/src/ui/component-objectui-held-typed-members.pin.test.ts(new): §1 15 byte-identical parses of the census writers, plus the absent-member case; §2 17 refusals bycodeand path, plus the two prescriptions, the no-prescription control and the alias pointers; §3 each shape's exact member set, and the two forms'fieldsanswering identically, messages included; §4 the three D3 ids.packages/spec/src/ui/component-props-unknown-members.pin.test.ts: four lines leave the ledger (markers[],mapping, bothfields[]). Seven becomeforklines naming their shapes (customFields, both forms'sections,items, both action containers' members, and the drillreport, which wasobjectui-held). Theobjectui-heldstage is replaced byfork, and a §2 case is added.packages/spec/src/ui/component-form-family-typed-members.pin.test.ts: the header records this stage's outcome.packages/spec/src/migrations/entries/semantic/18.ui-object-gantt-markers-typed.ts,18.ui-object-timeline-mapping-typed.ts,18.ui-object-form-fields-names-typed.ts(new), andpackages/spec/src/migrations/registry.ts: three step-18 rationale fragments at orders 74, 75 and 76 (73 is the last taken onmain), inserted where each id sorts. The semantic region is regenerated bygen:migration-registry. No D2 conversion and noRETIRED_KEYS_BY_MAJORrow: page-componentpropertiesis not parsed on the save or load path, and the refused values are nested member values.check:generated --fix, which proved only these two stale:content/docs/references/ui/component.mdxanddocs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md(ui/196 → 198 sites, +2 strict)..changeset/21464-component-props-objectui-held-typed.md:@objectstack/specminor, BREAKING banner, theClause-②line, FROM → TO, the census, and the ADR-0087 marker registering the three ids.Measurements
Head
723df54741, base7d0781482d. Heavy runs went throughscripts/pm/os-verify-lock.sh, and every exit code was captured before any pipe.@objectstack/spec@17.6.0(the npm tarball,ComponentPropsMap[type].safeParse). Each of these is ACCEPTED there:markers: [{ date: 5 }],[{ label: 'Deadline' }],[{ date, title }];mapping: 'subject',{ titleField: 'code' },{ title: 5 }; object-formfields: [{ name, label, required }],[{ field: 'note' }],[5]; master-detailfields: [{ name: 'note' }, 'status']. Each is refused on this branch, pinned in §2.pnpm --filter @objectstack/spec testat723df54741: exit 0, Test Files 611 passed (611), Tests 18157 passed, 1 todo, nothing skipped. An earlier run ata88b9f3e4aread 610 passed and 1 skipped:root-entry-type-nameability.pin.test.tsskips by design whiledistis stale, and it ran here after a rebuild.pnpm --filter @objectstack/spec typecheckata88b9f3e4a(only a test comment differs at the head): exit 0,check:test-typecheck: OK(52 files / 246 errors / 135 signatures held).tsc -p tsconfig.test.json --listFilesOnlynames the three touched pins.gen:schemapass.check:generatedprovedcheck:docsandcheck:strictness-ledgerstale,--fixregenerated those two only, and the re-check is green. A laterturbo run build --filter=!@objectstack/docsbuilt 72 tasks, all successful.pnpm --filter @objectstack/lint testpassed 119 files and 5622 tests. The 5 skipped tests read lint's own builtdist, which this worktree had not built yet. After the turbo build,lazy-deps,runtime-lazy-depsandvalidate-component-propspassed 61 of 61.pnpm --filter @objectstack/example-showcase validateprinted "Validation passed".validateComponentPropsfrom lintsrcover the built spec, one block per page). These report nothing: a marker withdate/label/color, a{ title, variant }mapping, form and master-detail name lists, and an arbitrary drillreport(still held). These are reported:markers.0.date: 5ascomponent-props-invalid; a markertitleascomponent-props-unknown-keyatproperties.markers.0.title, its message naminglabel;mapping: 'subject'ascomponent-props-invalid;mapping.titleFieldascomponent-props-unknown-key; a form{ name: 'email', … }entry ascomponent-props-invalidatproperties.fields.0, with "write'email', not an object"; and a master-detail{ field: 'note' }entry with the section-vocabulary prescription.a88b9f3e4a). Each leg usednode scripts/ablation-replace.mjsin wrap mode, inside a driver with its ownEXIT INT TERMrestore trap on the absolute path, an empty hash read as failure, and HEAD blob1a6b65102a:markers→z.array(z.unknown()): anchor x1 → x0, blob →1f6bdbd1a9. Red: 9 failed, 86 passed. The enumeration pin's §1 received exactly[ "object-gantt markers[]" ]and its census-equals-ledger control read 88 against 87. The companion pin failed its five marker refusals, the alias pointer and the §3 member set.mapping→z.unknown(): blob →2ee8a60e25. Red: 8 failed, 87 passed. §1 received[ "object-timeline mapping" ], 88 against 87, plus the four mapping refusals, the alias pointer and §3.fields→z.array(z.unknown()): blob →5211b43913. Red: 9 failed, 86 passed. §1 received[ "object-form fields[]" ], 88 against 87, plus the four refusals, both prescriptions and the §3 two-forms identity.git diff HEADis empty. The pins import./component.zodfrom source, so no build ordistpreflight sits between mutation and run.723df54741:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 114 commands, all exit 0.--ranprinted "114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN". The first pass reddened onlycheck-spec-docblock-symbol-anchors(and its--self-test) on a line anchor in the new pin's comment; it now cites the file. Every command was re-run on the final head.check-widening-tellsover the branch diff:--declaration noexits 4 with seven T1 tells, every one a key inside a formerz.unknown()bag (the marker's three, the mapping's four).--declaration yesexits 0.eslint --no-inline-config --format jsonover the 8 changed TypeScript files reports 8 files, 0 errors, 0 warnings. Population comes from eslint's own--print-config: those 8 resolve a config, and the changeset,component.mdxand the counts page resolveundefined. Invariance:eslint.config.mjsnever enables type-aware linting (its own text, about line 327), so this diff cannot move a verdict on an untouched file. The fullpnpm lintis CI's.pnpm lint, because they are CI-owned; objectui was read at the pin and atmain, not built against this spec. CI on this PR was not waited on.Acceptance notes
ReportSchemaadmits a joined report with no dataset-bound block.objectstack validateexits 0 onreports: [{ name, label, type: 'joined', blocks: [{ name, label }, { name, label }] }]. The same report with a bound block naming an undeclared dataset is refused bychart-dataset-unknown. objectui's report renderer sends it pastisDatasetReport(plugin-report/src/DatasetReportRenderer.tsx:208-214) to the pre-9.0 presentation bridge (ReportRenderer.tsx:108), which issues no query. The schema's own refinement comment says a joined report "carries its data onblocks(each block dataset-bound)". This is reported in the dev report for the seat to file, and it is the cheapest route out of thereportfork.mapping.title/mapping.dateare second spellings oftimeline.titleField/timeline.startDateField, read between those and the flat fallbacks. They are typed here as objectui declares them; whether to retire them is a different question. Noted, not filed.dateis any string, as objectui declares it and as the same row'sholidays/minDate/maxDateare. An unparseable date is still accepted and draws no line. Noted, not filed.@objectstack/specbump: its block schemas take these rows by reference (propsBagoverstripImportedDefaults(...)), but no objectui test parses a refused value through them. The refused values are fixtures that mount the component or parse objectui's own mirror. objectui source reads none of the four members' narrowed types.Generated by Claude Code