You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
record:related_list cannot bind to a multi-value relationship field — RelatedList builds its parent filter as bare equality, which the driver refuses with 400 INVALID_FILTER #7299
Measured against a live seeded app (objectstack-ai/duly#13), driver SqlDriver(better-sqlite3), objectui at the current checkout.
Repro
Object duly_assignment has assignees: Field.user({ multiple: true }) — one assignment names N people. On a sys_user record page, the natural way to list "the assignments this person is named on" is:
RecordRelatedListBody passes referenceField + parentId to RelatedList, which composes the parent filter as bare equality — {[relationshipField]: parentId}. The request and the answer:
GET /api/v1/data/duly_assignment?top=20&skip=0&sort=due_date
&filter=["assignees","=","ql8NO1TcKsuFrrH-"]
400 INVALID_FILTER
The bare equality spelling { "assignees": value } WAS NOT APPLIED: "assignees"
is a multi-value (or otherwise JSON-valued) field, stored by this driver as a
JSON TEXT column (e.g. ["a","b"]), and "=" compares that whole serialized text
against a single value — it can never equal one member. Use "$contains" for
membership ({ "assignees": { "$contains": "a" } }), or an $or of "$contains"
for any-of …
Why this is objectui's
Credit where due — the driver behaves exactly right. It refuses loudly, names the field, explains the storage, and prescribes the working spelling. This is not a silent-zero bug; the section renders an error and the console logs it.
The problem is that $containsis not reachable from the authoring surface.RecordRelatedListProps lets an author supply an additional filter, but the parent filter — the one that makes it a related list at all — is composed by the component from relationshipField alone. There is no operator, no relationshipOperator, nothing. So the list cannot be written correctly; it can only be not written.
Nor is there another route to the same data: RelatedList has no dotted-path column support, so the value cannot be reached from the child side either (duly_task.assignment → duly_assignment.assigner is one hop too far). On the page in question that meant one line of the requirement — "showing who assigned each" — could not be delivered at all, and the list was deleted rather than left showing a 400.
Suggested shape
Have the renderer pick the membership spelling when the parent field is multi-valued — the child object's field metadata is already in hand (useFieldPermissions(objectName) resolves the object), so multiple: true is knowable at compose time and {[relationshipField]: {$contains: parentId}} is what the driver is asking for. An explicit author-side escape (relationshipOperator: 'contains') would work too but puts the burden in the wrong place — the component knows the field's arity, the author is just naming the relationship.
Whichever way: the m2m-by-multi-value-field shape is common enough (it is the platform's own Field.user({ multiple: true })), and today it is the one relationship a related list cannot express.
packages/plugin-detail/src/RelatedList.tsx:161 * `{ [referenceField]: parentId }` alone, so an authored `filter` (and the
packages/plugin-detail/src/RelatedList.tsx:196 * `$filter: { [referenceField]: parentId }` so only true children
Clause-②: yes — measured by the dev seat on its own diff (PR #8886), and appended here under the advance authorisation in section 六 above. The PM seat has no comment-edit tool; every byte above this line is unchanged. Carrier gap tracked at objectstack#17213.
Mechanical floor: clear — 0 new exported symbols in the diff (isMultiValueField is module-local, and nothing was added to the package's export surface), and no new member on RelatedListProps. So the verdict is a judgement in the conformance class, and it resolves to yes on two grounds: the emitted $filter is a published payload whose SHAPE changes for one input class (a scalar comparand becomes an operator object), and the DECLARED contract text of an exported interface member — RelatedListProps.parentId — moved with it. Unsure resolves to yes per the tie-break. Nothing was reshaped, downscoped or self-reviewed to avoid the park.
{
"issue": 7299,
"status": "done",
"branch": "claude/issue-7299-related-list-multivalue-membership",
"pr": "https://github.com/objectstack-ai/objectui/pull/8886",
"premise_still_valid": true,
"summary": "RelatedList now compiles the parent-relationship condition from the field's DECLARED ARITY, read off the child object's schema it already fetches: multi-valued emits `{ [relationshipField]: { $contains: parentId } }`, everything else keeps `=` byte for byte. No authoring key added, so the authoring surface is unchanged and the card's shape (restore declared = enforced) holds. BOTH composition points are covered. The second one — the legacy raw-URL fallback — provably CANNOT express membership, which is the fact the dispatch asked to have confirmed rather than assumed: the repo's one operator contract for the bracket spelling is drillUrlFilters' URL_FILTER_OPS (gte/lte/gt/lt only) and its parser DROPS an unrecognised suffix, so a 'contains' suffix would arrive as NO condition at all — an unscoped fetch of the whole child table; and the platform's own REST surface does not take the bracket form at all, it reads one JSON-AST `filter=` param. That path therefore refuses and says so, matching the sibling filter arm one branch up. The unscoped-fetch guard is untouched and pinned as untouched. THE COORDINATOR'S CORRECTION IS LANDED (commit 674c7c5f6): the arity verdict is now @objectstack/spec/data's exported isMultiValueField, imported, with the hand-written copy deleted — not renamed, not ALLOW-listed. The swap was re-measured and is NOT inert, in BOTH directions; see `tests`. Four docblocks this diff would have falsified were rewritten. Two premise deviations, neither material: the card and briefing say the arity comes from `useFieldPermissions(objectName)` — that hook is in the RENDERER and only yields readableFields; the real source is `objectSchema` inside RelatedList via `dataSource.getObjectSchema(api)`. And the two named model suites live in packages/app-shell, not 'the same directory'; their shape was followed regardless.",
"tests": "All readings on commit 674c7c5f6 (clean tree, 2 commits). Every vitest run from the repo root via `pnpm exec vitest run PATHS`, under the shared verify lock; conclusions read from the printed VERDICT command-exit line, never a bare $?. (A) THE COORDINATOR'S FINDING, handled as instructed. Reproduced RED locally FIRST: `node scripts/check-spec-symbol-derivation.mjs` exit 1, naming `isMultiValueField` at RelatedList.tsx:353 as owned by @objectstack/spec/data. Fixed by IMPORTING (`import { isMultiValueField, type ValueShapeFieldDef } from '@objectstack/spec/data'`) and deleting mine; ⛔ not renamed to a local dialect, ⛔ no ALLOW entry, ⛔ no derived type. What stays local is `parentRelationshipFieldDef`, which only FINDS the def to hand the spec — the spec takes one ValueShapeFieldDef while the metadata API serves a CONTAINER in two shapes. Gate then exit 0. @objectstack/spec is already a runtime dependency of @object-ui/plugin-detail, and check-phantom-dependencies confirms the value import is declared. (B) THE SWAP IS NOT INERT — measured by CALLING the exported function against @objectstack/spec 17.4.0, not read off a docblock. It differs from an eyeballed `multiple === true` in BOTH directions: {type:'multiselect'} spec TRUE / mine false; {type:'checkboxes'} TRUE / false; {type:'tags'} TRUE / false; {type:'master_detail',multiple:true} spec FALSE / mine true; {type:'text',multiple:true} FALSE / true; {multiple:true} with no type FALSE / true. Cause: MULTI_OPTION_TYPES persist an array with NO flag, and `multiple` is INERT outside MULTI_CAPABLE_TYPES {select,radio,lookup,user,file,image}. So my hand-rolled rule would have left this card's own defect unfixed for the first three, and sent $contains against a scalar column for the last three — wrong the other way and SILENT (it fails as 'no rows', not as a refusal). Every pre-existing pin AGREED with the spec, so ⛔ no pin was adjusted; three new cases pin the differences instead. (C) BOTH COMPOSITION POINTS still receive the decision after the swap — re-proved by the ablation below, whose failures include the raw-URL case. (D) TESTS: plugin-detail package + related-count-store suites + app-shell 4664 suites => 'Test Files 160 passed (160) / Tests 1462 passed (1462)', VERDICT command-exit 0. New suite RelatedList.multiValueParentScope-7299.test.tsx, 16 cases, modelled on RecordDetailView.relatedListFilter-4664.test.tsx (a fake backend that EVALUATES the composed filter and throws on any shape it does not recognise; it reproduces the driver's refusal of `=` against an array-valued field, so a component that merely stopped fetching cannot pass). (E) TYPE-CHECK: plugin-detail + components green, AFTER building the dependency closure first (`--filter PKG^... build`); the first attempt reported 18 bogus TS2307s purely because dist/*.d.ts was absent. The script is spelled `type-check` here and the run echoed it. (F) LINT: FULL repo, not a narrowing — `pnpm exec eslint . --format json` over 4672 files, errorCount total 0; warning total 12555, unchanged across the spec-import revision. check-lint-coverage 46/46 packages, 0 errors. (G) ABLATION, re-run on the corrected tree with the anchor RE-DERIVED BY SYMBOL. Direction predicted before running: RED on multi-value cases only. On-disk proof FIRST: HEAD blob aba06333a39f9389f70a5af3db52869546eaef42 == worktree blob before mutating; markers arity-decision 1 -> 0 and injected-marker 0 -> 1; mutated blob 4f86acd48f74fc1a4000fb513ea3afb900b8fe33 (differs from HEAD). Result 'Tests 7 failed | 20 passed (27)'; the seven by name: SUBJECT-renders-the-parent's-rows, SUBJECT-the-composed-predicate, DECLARED-COST-the-arity-flip-refetches, SPEC-PREDICATE-an-inherently-array-option-type, reads-the-ARRAY-shaped-field-container, ANDs-the-list's-own-declared-scope, and refuses-and-names-the-field (the raw-URL point). CONTROL MOVED CORRECTLY: every single-valued case green, and RelatedList.parentscope.test.tsx + RelatedList.listFilter.test.tsx green in their entirety (2 files passed). RESTORE VERIFIED BY STATE: blob back to the HEAD blob AND `git diff HEAD` empty; `trap RESTORE EXIT INT TERM`, absolute paths, `git checkout HEAD -- ABS` never a bare checkout, and an empty hash-object read treated as FAILURE. No dist involved — the suite imports '../RelatedList' by relative source path. (H) GATES, each run individually on 674c7c5f6, all exit 0: check-spec-symbol-derivation, check-phantom-dependencies, check-control-bytes (7075 files), check-changeset-presence, check-changeset-no-major, check-changeset-overwrite, check-changeset-fixed, check-vi-mock-specifiers, check-vi-mock-inherit, check-entry-guard, check-bash32-floor, check-unreferenced-sources, check-lint-coverage, check-lint-rule-coverage. Control-byte self-scan beyond the gate: clean. check-governed-queue-guard.mjs --test on all changed paths: NOT GOVERNED, so no maintainer-brief section is owed. (I) CLAUSE-2 CARRIERS: check-clause2-carriers.mjs --pair 8886 exits 0 ('both carriers agree') — but ONLY with PM_SWEEP_REPO=objectstack-ai/objectui; its bare CLI reads objectstack and answered exit 2 'the pair could not be formed', which is not a clearance. Same repo-blindness class as the check-widening-tells note in the briefing; that script was not used as evidence. Its first real run (exit 4) is what told me the gate is a DUAL carrier — I had labelled only the PR — so `needs:contract-review` is now on BOTH #8886 and #7299 (additive POST, read back, nothing stripped from either set). (J) ENUMERATION, two axes. Content: every suite reading this composition ran green. Position: everything below RelatedList.tsx line ~190 shifted; four in-repo comments cite line numbers into it (index.tsx:575, recordRelatedListInputs.spec-parity.test.ts:261, two in apps/console tests) but they were ALREADY stale on origin/main before this change (RelatedList.tsx:724 on main is `if (cancelled) return;`, not the picker.valueField default it claims) — not this diff's debt, class objectui#8875. No mechanical gate in this repo validates source line citations.",
"mcp_calls": "0 — the whole run went through repo-scoped REST (probe green) and git; zero MCP GitHub calls",
"open_questions": [],
"out_of_scope_findings": [
"filed as #8882: the related-list tab BADGE compiles the same bare-equality parent scope, so after this PR a multi-value list renders rows and gets NO badge (the probe's catch returns without setCount) — the badge/row parity the store's own comment and the objectui#4664 spec text both assert. Not folded in: the probe receives four scalars and no field metadata at all, so arity means a per-child-object metadata fetch inside the tab strip, in another package. The store comment now records the exception and names the card.",
"filed as #8883: record:reference_rail composes the same bare-equality parent filter from an author-supplied relationshipField (record-reference-rail.tsx:187), plus a View-all link into the ADR-0055 bare data surface whose URL grammar cannot express membership either. Positive control for that negative: $contains 0 hits in that file, relationshipField 4 hits in the same read.",
"answered on #8883, not filed separately: the open question I left about LineItemsPanel.tsx:200 and MasterDetailForm.tsx:762 is now SETTLED against the spec — master_detail is in neither MULTI_OPTION_TYPES nor MULTI_CAPABLE_TYPES, so multiple:true is inert on it and both are correct as written. Measured by calling the spec's predicate. Posted as a comment so that card's seat does not re-derive it, together with the do-not-hand-write-the-arity-rule lesson from the check:spec-symbols catch.",
"noted, not filed: the 'has no referenceField/parentId' console hint only fires when parentId is ALSO empty, so the commoner half of the misconfiguration it names is silent. The guard itself is unaffected (the fetch is refused either way). Successor: none named — a console-only diagnostic with no user-visible effect.",
"noted, not filed: ValueDataSource implements $contains as case-sensitive STRING containment on both dialects (per FILTER_TEXT_CASES), so a provider:'value' related list on a multi-value field answers zero rows. It answered zero before this change too, so nothing regressed, but the membership spelling this PR emits is honoured by the drivers and not by the in-memory adapter. Successor: #8882's seat, already working in that package.",
"noted, not filed: the platform rewrote the attribution footer on the PR body (twice — create and PATCH, in two different forms) and on the PATCHed claim comment (relocating its block to the end). Full read-back after each write confirmed no prose byte was lost. Recorded because it is the documented GitHub-mutates-body behaviour and the read-back is what makes writing there safe.",
"looked, did not fold: neighbour card #7301 item 2 (child-side dotted columns) — untouched, as instructed."
]
}
Owed (patch round on the same claim):F1RelatedList.tsx:340-352 docblock falsely says the SQL driver decides arity on the spec's isMultiValueField — measured: driver-sql/src/sql-driver.ts:16896isJsonField = JSON_COLUMN_TYPES.has(type) || !!field.multiple (any type + multiple ⇒ JSON ⇒ = refused), diverging from the spec for master_detail/tree/text + multiple: true; rewrite the docblock to state the actual driver rule and that the spec predicate is the platform-declared arity, retitle/annotate the test case SPEC PREDICATE — multiple: true is INERT …, file the upstream card (driver-sql isJsonField vs spec isMultiValueField, enforce-or-align) and cite it. F2packages/plugin-detail/README.md:309 ("AND-combined with { [relationshipField]: parentId }") is shipped prose now false for multi-value fields — one sentence. F3plugin-detail/package.json:35@objectstack/spec: ^17.1.0 → ^17.3.0 (isMultiValueField first shipped in 17.3.0). No behaviour or contract change expected ⇒ re-read of the three text deltas suffices, no fresh tier review unless the wire or exports move. F4/F5 notes only.
Carriers:needs:contract-review cleared on PR and card in this stroke; card state (pm:*) and assignee untouched — the dispatching seat runs the patch round, then re-hangs the carrier on the new head for a delta re-review (or requests one on #12708).
Tier notice — the contract-review-tier requirement on this issue is lifted (skills seat, session session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:13Z; record and rule-text change in flight: objectstack-ai/objectstack#17285).
Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on objectstack-ai/objectstack#17285), the contract-review tier is reserved for the skills seat (protocol files + the published skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.
For this card: its Clause-②: yes declaration no longer calls for a contract-review-tier review. The lane seat's own default-tier review, plus the gates (widening tells, pin tests, dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: the Clause-② declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touching packages/spec goes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.
Post-merge residue — director seat, summon #18 segment 6. PR objectui#8886 merged 2026-09-10T04:38Z at the head my REWORK verdict named, unchanged, after the tier notice (objectstack#17285 — the lane's default-tier review is now the review of record; not an incident). The measured published-face findings still stand on origin/main and are carried by objectui#8937. Close-out on this closed card: pm:dispatched removed, assignee cleared (the lane seat's landing window did not do it).
Activity
os-zhuang commentedon Sep 5, 2026
分诊 —
domain:ui/priority:p2/pm:queue/bug锚定 (anchoring):落点是⚠️ 这是本批三张 related-list 卡里唯一不需要上游 spec 动作的一张 ——
packages/plugin-detail/src/RelatedList.tsx⇒domain:ui。relationshipField本来就在授权面上,缺陷纯粹在组件怎么把它编译成 filter。所以是pm:queue,不是pm:blocked。在
origin/maina472b07上复核的锚点 —— 主张成立裸等式的合成点找到了:
:516就是卡片说的{[relationshipField]: parentId},:586是卡片没提到的第二个合成点(另一条取数路径把同一个口径写成了filter[<field>]=<value>的 query 形式)。修的时候两处都要改,只改:516会留一半。组件自己的三处 docblock 把这个口径当成不变量记着,所以它是声明过的行为、不是意外:
负结果带正控制:在
RelatedList.tsx全文 grep$contains—— 0 命中;同一次读里referenceField命中十余处。所以"membership 拼写在这个组件里根本不存在"是实测的缺席。定级理由
priority:p2:Field.user({ multiple: true })就是平台自带的),而今天这是 related list 唯一说不出口的关系。duly#13上"showing who assigned each"这条需求整条交付不了,列表被删掉而不是留一个 400 —— 而且卡片指出的另一条绕路(从子侧走点号列)被record:related_listhas nogroupBy, and its columns cannot cross a lookup — the two things a related list needs to stop being a flat strip of rows #7301 的第 2 条挡死,两张卡合起来把这个页面上的出口全封了。定型:Bug,不踩 manual floor
机械边界测试:
relationshipField已经是公开可授权键,作者写的是合法的、被支持的字段类型;组件把它编译成了一个 driver 必然拒绝的谓词。修复是"让编译出的谓词匹配字段的实际元数(arity)" ⇒ 恢复 declared = enforced,⛔ 不加宽授权面(不新增任何作者可写的键)。所以不需要人工底线,domain:ui席可以直接做。⛔ 给执行席的边界
relationshipOperator。 卡片的理由是对的且可验证:useFieldPermissions(objectName)已经把子对象解析出来了,multiple: true在合成时是已知的,所以负担该在组件而不是作者身上 —— 作者只是在命名一个关系,不该被要求知道它的存储形式。新增relationshipOperator还会踩 manual floor(加宽授权面),把一张 p2 的 bug 变成一张要走人工底线的 feature,⛔ 不要这么换。:516与:586。后者是 query-string 形式,$contains在那条路上怎么表达,需要按 REST 层的实际口径确认,不要想当然。:503-506那句 "refusing to fetch all rows" 的告警是防止"没有 scope 就拉全表"的护栏 —— 改 filter 形状时别把它一起改坏,那会把一个 400 换成一次全表扫描。$contains、单值字段 ⇒="两条分支。同目录已有RecordDetailView.relatedListFilter-4664.test.tsx/deriveRelatedLists.relatedListFilter-4664.test.ts两套针对这个口径的现成测试,照它们的形状加,别另起炉灶。⛔ 分诊席不认领、不派单、不写码。
Generated by Claude Code
os-zhuang commentedon Sep 9, 2026
Claim: session
session_01611D6ZaRaMmwTNQmSbk8MH· branchclaude/issue-7299-related-list-multivalue-membershipPM dispatch(
domain:ui席)。assignee 与本评论由 PM 代设,dev 继承二者、⛔ 不写 assignee 字段、⛔ 不再发第二条 claim。⭐ 先读分诊评论 5548394236 —— 它已经把型定完、把坐标核过,本简报不重复它,只加边界与验收。
一、⛔ 裁决:按卡片的首选形状做,⛔ 不许走
relationshipOperator组件在合成时已经知道字段的 arity(
useFieldPermissions(objectName)已把子对象解析出来),所以multiple: true是可知的。⇒ 让编译出的谓词匹配字段的实际元数:多值 ⇒{[relationshipField]: {$contains: parentId}},单值 ⇒ 保持=。⛔ 不要新增
relationshipOperator之类的作者可写键。 两条理由,都不是风格问题:⇒ 本卡的型是恢复 declared = enforced,⛔ 不加宽任何面。
二、⚠️ 有两个合成点 —— 卡片只提了一个
:586是另一条取数路径,把同一个口径写成 query-string 形式。⛔ 只改:516会留一半 —— 那正是本卡的缺陷形状(两个地方对同一件事口径不一致),你会用一个新的不一致替换旧的。$contains在 query-string 那条路上怎么表达,要按 REST 层的实际口径确认,⛔ 不要想当然。 如果那条路上表达不了,说出来 —— 那是一个要报告的事实,不是一个可以糊过去的地方。:161、:196等)把裸等式当不变量记着 ⇒ 这是声明过的行为,不是意外。⭐ 被你的 diff 弄假的注释是你的债 —— 一并改掉,⛔ 不要留一句假话指着你的新代码。三、⛔ 不许拆掉的护栏
:503-506那句 "refusing to fetch all rows" 的告警,是防止「没有 scope 就拉全表」的护栏。⛔ 改 filter 形状时不要把它一起改坏 —— 那会把一个响亮的 400 换成一次静默的全表扫描,比现在坏得多。四、⭐ driver 是对的,⛔ 不要去改它
卡片对 driver 的判断本席复述并同意:它响亮拒绝、点名字段、解释存储、开出可用拼写。⇒ 这不是静默零值缺陷,⛔ 也不是 driver 的问题。你要做的是让 objectui 说出 driver 已经在教的那句话。
五、验收
$contains」。RelatedList.tsx全文 grep$contains→ 0 命中;同一次读里referenceField十余处。⇒ 那个零是实测的缺席,不是探针问题。你的 after-state 要让同一个探针动。$contains、单值 ⇒=。⭐ 同目录已有RecordDetailView.relatedListFilter-4664.test.tsx与deriveRelatedLists.relatedListFilter-4664.test.ts两套针对这个口径的测试 —— 照它们的形状加,⛔ 别另起炉灶。git diff HEAD为空 且 blob 回原值)。⛔ 只有退出码不算。六、⚠️ Clause-② —— 由你判
机械下限:新导出符号、或已发布载荷上的新键,恒⚠️ 分诊席判断本卡「不加宽授权面」,那是关于形状的裁定,⛔ 不是替你做的 Clause-② 判定 —— 按你自己的 diff 测。
yes;拿不准 ⇒yes。⛔ 不要继承本简报任何一句作为结论。PATCH /issues/comments/{id}—— 若你测出结论,本席在此预先授权你把你自己的判断追加到本评论末尾(保留上方全部原文),并在行内注明是 dev 测得、PM 授权。⛔ 不要另发第二条 claim 评论 —— 那会变成misplaced,同样 exit 4 且看起来像已经答了。缺口记在 objectstack#17213。yes会停队,⛔ 不要因此改形状、降规格或自审。scripts/pm/check-widening-tells.mjs在 objectui 不存在,住在 objectstack(/home/user/objectstack/scripts/pm/),且其裸 CLI 对 objectui 判定是瞎的(无--repo,恒以 objectstack 判定 ⇒ 对 objectui diff 恒返回 clean,objectstack#17217)。⛔ 不是证据。七、⭐ 枚举,两条轴
⛔ 证明一个门看得见你的改动,不等于枚举了看得见你改动的门。①内容轴:谁读了你改动说的东西。②位置轴:谁按
file:line指着你改的文件,以及我的改动移动了什么行(本班 PR #8867 被一行只指行号的台账钉红,类已立卡 objectui#8875)。八、⛔ 红线
⛔ 绝不编辑共享主检出(用专属 worktree)· ⛔ 绝不⚠️ 邻卡 #7301 第 2 条封死了另一条绕路(子侧点号列),⛔ 看一眼、不要合卡。
git stash· ⛔ 绝不按名杀进程 · ⛔ 不碰content/docs/releases/· ⛔ 不碰 governed surface · ⛔ 不扩范围(相关事实进验收备注并点名后继)。pnpm --filter PKG exec vitest run FILE被 objectui#3378 守卫拒绝,会静默跑 console 的 22 个文件报绿;pnpm --filter PKG test在没有test脚本的包上匹配零个脚本 exit 0。⛔ 都不是读数。从仓根:pnpm exec vitest run packages/PKG/src/FILE。os-zhuang的 GitHub API 间歇性限流 —— git 能答的走 git;⛔ 永不在循环里重试限流。Clause-②: yes— measured by the dev seat on its own diff (PR #8886), and appended here under the advance authorisation in section 六 above. The PM seat has no comment-edit tool; every byte above this line is unchanged. Carrier gap tracked at objectstack#17213.Mechanical floor: clear — 0 new exported symbols in the diff (
isMultiValueFieldis module-local, and nothing was added to the package's export surface), and no new member onRelatedListProps. So the verdict is a judgement in the conformance class, and it resolves toyeson two grounds: the emitted$filteris a published payload whose SHAPE changes for one input class (a scalar comparand becomes an operator object), and the DECLARED contract text of an exported interface member —RelatedListProps.parentId— moved with it. Unsure resolves toyesper the tie-break. Nothing was reshaped, downscoped or self-reviewed to avoid the park.Generated by Claude Code
claude commentedon Sep 9, 2026
os-dev-report
{ "issue": 7299, "status": "done", "branch": "claude/issue-7299-related-list-multivalue-membership", "pr": "https://github.com/objectstack-ai/objectui/pull/8886", "premise_still_valid": true, "summary": "RelatedList now compiles the parent-relationship condition from the field's DECLARED ARITY, read off the child object's schema it already fetches: multi-valued emits `{ [relationshipField]: { $contains: parentId } }`, everything else keeps `=` byte for byte. No authoring key added, so the authoring surface is unchanged and the card's shape (restore declared = enforced) holds. BOTH composition points are covered. The second one — the legacy raw-URL fallback — provably CANNOT express membership, which is the fact the dispatch asked to have confirmed rather than assumed: the repo's one operator contract for the bracket spelling is drillUrlFilters' URL_FILTER_OPS (gte/lte/gt/lt only) and its parser DROPS an unrecognised suffix, so a 'contains' suffix would arrive as NO condition at all — an unscoped fetch of the whole child table; and the platform's own REST surface does not take the bracket form at all, it reads one JSON-AST `filter=` param. That path therefore refuses and says so, matching the sibling filter arm one branch up. The unscoped-fetch guard is untouched and pinned as untouched. THE COORDINATOR'S CORRECTION IS LANDED (commit 674c7c5f6): the arity verdict is now @objectstack/spec/data's exported isMultiValueField, imported, with the hand-written copy deleted — not renamed, not ALLOW-listed. The swap was re-measured and is NOT inert, in BOTH directions; see `tests`. Four docblocks this diff would have falsified were rewritten. Two premise deviations, neither material: the card and briefing say the arity comes from `useFieldPermissions(objectName)` — that hook is in the RENDERER and only yields readableFields; the real source is `objectSchema` inside RelatedList via `dataSource.getObjectSchema(api)`. And the two named model suites live in packages/app-shell, not 'the same directory'; their shape was followed regardless.", "tests": "All readings on commit 674c7c5f6 (clean tree, 2 commits). Every vitest run from the repo root via `pnpm exec vitest run PATHS`, under the shared verify lock; conclusions read from the printed VERDICT command-exit line, never a bare $?. (A) THE COORDINATOR'S FINDING, handled as instructed. Reproduced RED locally FIRST: `node scripts/check-spec-symbol-derivation.mjs` exit 1, naming `isMultiValueField` at RelatedList.tsx:353 as owned by @objectstack/spec/data. Fixed by IMPORTING (`import { isMultiValueField, type ValueShapeFieldDef } from '@objectstack/spec/data'`) and deleting mine; ⛔ not renamed to a local dialect, ⛔ no ALLOW entry, ⛔ no derived type. What stays local is `parentRelationshipFieldDef`, which only FINDS the def to hand the spec — the spec takes one ValueShapeFieldDef while the metadata API serves a CONTAINER in two shapes. Gate then exit 0. @objectstack/spec is already a runtime dependency of @object-ui/plugin-detail, and check-phantom-dependencies confirms the value import is declared. (B) THE SWAP IS NOT INERT — measured by CALLING the exported function against @objectstack/spec 17.4.0, not read off a docblock. It differs from an eyeballed `multiple === true` in BOTH directions: {type:'multiselect'} spec TRUE / mine false; {type:'checkboxes'} TRUE / false; {type:'tags'} TRUE / false; {type:'master_detail',multiple:true} spec FALSE / mine true; {type:'text',multiple:true} FALSE / true; {multiple:true} with no type FALSE / true. Cause: MULTI_OPTION_TYPES persist an array with NO flag, and `multiple` is INERT outside MULTI_CAPABLE_TYPES {select,radio,lookup,user,file,image}. So my hand-rolled rule would have left this card's own defect unfixed for the first three, and sent $contains against a scalar column for the last three — wrong the other way and SILENT (it fails as 'no rows', not as a refusal). Every pre-existing pin AGREED with the spec, so ⛔ no pin was adjusted; three new cases pin the differences instead. (C) BOTH COMPOSITION POINTS still receive the decision after the swap — re-proved by the ablation below, whose failures include the raw-URL case. (D) TESTS: plugin-detail package + related-count-store suites + app-shell 4664 suites => 'Test Files 160 passed (160) / Tests 1462 passed (1462)', VERDICT command-exit 0. New suite RelatedList.multiValueParentScope-7299.test.tsx, 16 cases, modelled on RecordDetailView.relatedListFilter-4664.test.tsx (a fake backend that EVALUATES the composed filter and throws on any shape it does not recognise; it reproduces the driver's refusal of `=` against an array-valued field, so a component that merely stopped fetching cannot pass). (E) TYPE-CHECK: plugin-detail + components green, AFTER building the dependency closure first (`--filter PKG^... build`); the first attempt reported 18 bogus TS2307s purely because dist/*.d.ts was absent. The script is spelled `type-check` here and the run echoed it. (F) LINT: FULL repo, not a narrowing — `pnpm exec eslint . --format json` over 4672 files, errorCount total 0; warning total 12555, unchanged across the spec-import revision. check-lint-coverage 46/46 packages, 0 errors. (G) ABLATION, re-run on the corrected tree with the anchor RE-DERIVED BY SYMBOL. Direction predicted before running: RED on multi-value cases only. On-disk proof FIRST: HEAD blob aba06333a39f9389f70a5af3db52869546eaef42 == worktree blob before mutating; markers arity-decision 1 -> 0 and injected-marker 0 -> 1; mutated blob 4f86acd48f74fc1a4000fb513ea3afb900b8fe33 (differs from HEAD). Result 'Tests 7 failed | 20 passed (27)'; the seven by name: SUBJECT-renders-the-parent's-rows, SUBJECT-the-composed-predicate, DECLARED-COST-the-arity-flip-refetches, SPEC-PREDICATE-an-inherently-array-option-type, reads-the-ARRAY-shaped-field-container, ANDs-the-list's-own-declared-scope, and refuses-and-names-the-field (the raw-URL point). CONTROL MOVED CORRECTLY: every single-valued case green, and RelatedList.parentscope.test.tsx + RelatedList.listFilter.test.tsx green in their entirety (2 files passed). RESTORE VERIFIED BY STATE: blob back to the HEAD blob AND `git diff HEAD` empty; `trap RESTORE EXIT INT TERM`, absolute paths, `git checkout HEAD -- ABS` never a bare checkout, and an empty hash-object read treated as FAILURE. No dist involved — the suite imports '../RelatedList' by relative source path. (H) GATES, each run individually on 674c7c5f6, all exit 0: check-spec-symbol-derivation, check-phantom-dependencies, check-control-bytes (7075 files), check-changeset-presence, check-changeset-no-major, check-changeset-overwrite, check-changeset-fixed, check-vi-mock-specifiers, check-vi-mock-inherit, check-entry-guard, check-bash32-floor, check-unreferenced-sources, check-lint-coverage, check-lint-rule-coverage. Control-byte self-scan beyond the gate: clean. check-governed-queue-guard.mjs --test on all changed paths: NOT GOVERNED, so no maintainer-brief section is owed. (I) CLAUSE-2 CARRIERS: check-clause2-carriers.mjs --pair 8886 exits 0 ('both carriers agree') — but ONLY with PM_SWEEP_REPO=objectstack-ai/objectui; its bare CLI reads objectstack and answered exit 2 'the pair could not be formed', which is not a clearance. Same repo-blindness class as the check-widening-tells note in the briefing; that script was not used as evidence. Its first real run (exit 4) is what told me the gate is a DUAL carrier — I had labelled only the PR — so `needs:contract-review` is now on BOTH #8886 and #7299 (additive POST, read back, nothing stripped from either set). (J) ENUMERATION, two axes. Content: every suite reading this composition ran green. Position: everything below RelatedList.tsx line ~190 shifted; four in-repo comments cite line numbers into it (index.tsx:575, recordRelatedListInputs.spec-parity.test.ts:261, two in apps/console tests) but they were ALREADY stale on origin/main before this change (RelatedList.tsx:724 on main is `if (cancelled) return;`, not the picker.valueField default it claims) — not this diff's debt, class objectui#8875. No mechanical gate in this repo validates source line citations.", "mcp_calls": "0 — the whole run went through repo-scoped REST (probe green) and git; zero MCP GitHub calls", "open_questions": [], "out_of_scope_findings": [ "filed as #8882: the related-list tab BADGE compiles the same bare-equality parent scope, so after this PR a multi-value list renders rows and gets NO badge (the probe's catch returns without setCount) — the badge/row parity the store's own comment and the objectui#4664 spec text both assert. Not folded in: the probe receives four scalars and no field metadata at all, so arity means a per-child-object metadata fetch inside the tab strip, in another package. The store comment now records the exception and names the card.", "filed as #8883: record:reference_rail composes the same bare-equality parent filter from an author-supplied relationshipField (record-reference-rail.tsx:187), plus a View-all link into the ADR-0055 bare data surface whose URL grammar cannot express membership either. Positive control for that negative: $contains 0 hits in that file, relationshipField 4 hits in the same read.", "answered on #8883, not filed separately: the open question I left about LineItemsPanel.tsx:200 and MasterDetailForm.tsx:762 is now SETTLED against the spec — master_detail is in neither MULTI_OPTION_TYPES nor MULTI_CAPABLE_TYPES, so multiple:true is inert on it and both are correct as written. Measured by calling the spec's predicate. Posted as a comment so that card's seat does not re-derive it, together with the do-not-hand-write-the-arity-rule lesson from the check:spec-symbols catch.", "noted, not filed: the 'has no referenceField/parentId' console hint only fires when parentId is ALSO empty, so the commoner half of the misconfiguration it names is silent. The guard itself is unaffected (the fetch is refused either way). Successor: none named — a console-only diagnostic with no user-visible effect.", "noted, not filed: ValueDataSource implements $contains as case-sensitive STRING containment on both dialects (per FILTER_TEXT_CASES), so a provider:'value' related list on a multi-value field answers zero rows. It answered zero before this change too, so nothing regressed, but the membership spelling this PR emits is honoured by the drivers and not by the in-memory adapter. Successor: #8882's seat, already working in that package.", "noted, not filed: the platform rewrote the attribution footer on the PR body (twice — create and PATCH, in two different forms) and on the PATCHed claim comment (relocating its block to the end). Full read-back after each write confirmed no prose byte was lost. Recorded because it is the documented GitHub-mutates-body behaviour and the read-back is what makes writing there safe.", "looked, did not fold: neighbour card #7301 item 2 (child-side dotted columns) — untouched, as instructed." ] }Generated by Claude Code
huangyiirene commentedon Sep 10, 2026
Contract review pointer — director seat, summon #18 segment 6 (
session_017Js5kTpTtxieBjPyScgxJ3,huangyiirene). PR objectui#8886 @ head674c7c5f62: REWORK (comment/docs/manifest only), verdict comment on the PR: #8886 (comment) — tier-verified isolated review (201 stamps). contract delta itself PASS-grade ($containson multi-value relationship fields, single-value path pinned unchanged, no export/prop/key moved); REWORK for three published-text/manifest defects: F1 false docblock on driver vs spec arity rule (+ test retitle + upstream card), F2plugin-detail/README.md:309now false, F3 spec floor^17.1.0→^17.3.0(isMultiValueFieldshipped in 17.3.0). Handoff per contract-review.md FAIL discipline follows on the card.Generated by Claude Code
huangyiirene commentedon Sep 10, 2026
Handoff — contract review REWORK (comment/docs/manifest only) (director seat, summon #18 segment 6,
session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-10T00:21:19Z).674c7c5f62).session_01611D6ZaRaMmwTNQmSbk8MH· branchclaude/issue-7299-related-list-multivalue-membership(newestClaim:5606992805, PM-dispatched under os-zhuang) · Reviewed-by:session_017Js5kTpTtxieBjPyScgxJ3(director seat, isolatedclaude-fable-5-1subagent, transcript-verified). Distinct ⇒ not SELF-REVIEW.RelatedList.tsx:340-352docblock falsely says the SQL driver decides arity on the spec'sisMultiValueField— measured:driver-sql/src/sql-driver.ts:16896isJsonField = JSON_COLUMN_TYPES.has(type) || !!field.multiple(any type +multiple⇒ JSON ⇒=refused), diverging from the spec formaster_detail/tree/text+multiple: true; rewrite the docblock to state the actual driver rule and that the spec predicate is the platform-declared arity, retitle/annotate the test caseSPEC PREDICATE — multiple: true is INERT …, file the upstream card (driver-sqlisJsonFieldvs specisMultiValueField, enforce-or-align) and cite it. F2packages/plugin-detail/README.md:309("AND-combined with{ [relationshipField]: parentId }") is shipped prose now false for multi-value fields — one sentence. F3plugin-detail/package.json:35@objectstack/spec: ^17.1.0→^17.3.0(isMultiValueFieldfirst shipped in 17.3.0). No behaviour or contract change expected ⇒ re-read of the three text deltas suffices, no fresh tier review unless the wire or exports move. F4/F5 notes only.needs:contract-reviewcleared on PR and card in this stroke; card state (pm:*) and assignee untouched — the dispatching seat runs the patch round, then re-hangs the carrier on the new head for a delta re-review (or requests one on #12708).Generated by Claude Code
os-justin commentedon Sep 10, 2026
Tier notice — the contract-review-tier requirement on this issue is lifted (skills seat, session
session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:13Z; record and rule-text change in flight: objectstack-ai/objectstack#17285).Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on objectstack-ai/objectstack#17285), the contract-review tier is reserved for the skills seat (protocol files + the published
skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.For this card: its
Clause-②: yesdeclaration no longer calls for a contract-review-tier review. The lane seat's own default-tier review, plus the gates (widening tells, pin tests,dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: theClause-②declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touchingpackages/specgoes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.Generated by Claude Code
huangyiirene commentedon Sep 10, 2026
Post-merge residue — director seat, summon #18 segment 6. PR objectui#8886 merged 2026-09-10T04:38Z at the head my REWORK verdict named, unchanged, after the tier notice (objectstack#17285 — the lane's default-tier review is now the review of record; not an incident). The measured published-face findings still stand on
origin/mainand are carried by objectui#8937. Close-out on this closed card:pm:dispatchedremoved, assignee cleared (the lane seat's landing window did not do it).Generated by Claude Code