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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(fields): two more undeclared-but-consumed metadata keys — dependsOn on select and options[].description on lookup, both reached past the declared type #6153
Measured while converting the remaining content/docs/fields pages for #6138 (PR 2). Filed unassigned, not fixed there — the conversion takes both keys out of the annotated blocks (they would redden them); whether they come back as declared members is this card's question.
Same class as #6140 (RichTextField's rows). Filed as one card rather than one per page, because the two instances are the same defect in the same layer and triage should grade them together.
Instance 1 — dependsOn, read off select field metadata through an as any
config is the field metadata. SelectFieldMetadata (packages/types/src/field-types.ts) declares options, multiple, searchable and nothing else of its own; BaseFieldMetadata declares depends_on?: string[] — the snake_case sibling — and no dependsOn. The camelCase spelling is declared only as a widget prop (FieldWidgetComponentProps.dependsOn?: DependsOnInput, widgets/types.ts:191), which is the ?? dependsOnProp half of that same line.
So an author who writes dependsOn on a select field's metadata gets working cascading options in the app and a key no exported metadata type accepts — declared and enforced disagree in the direction that hides. content/docs/fields/select.mdx documented it as a metadata key, which is how it was found.
The sibling widget reads both spellings explicitly: LookupField.tsx:292 is fieldMeta?.depends_on ?? fieldMeta?.dependsOn, so the same concept is reachable under two names there, one declared and one not.
Instance 2 — description on a lookup's static options
LookupFieldMetadata.options is declared as SelectOptionMetadata[]. SelectOptionMetadata declares label, value, color, icon, disabled, visibleWhen — no description.
LookupField.tsx:250 takes those authored options as the widget's own local type:
LookupOption (LookupField.tsx:38) does declare description?: string — but it is a widget-local interface with an open [key: string]: any, and it is not part of @object-ui/fields's published surface (no occurrence in packages/fields/dist/index.d.ts). The assignment above crosses from the declared metadata type to the open local one without a diagnostic because fieldMeta is untyped.
So a lookup's authored option description is real behaviour (it is searched, and recordToOption at :153 produces the same key for fetched options) that the declared option type rejects.
Candidate resolutions — deliberately not decided here
Both instances turn on the same question, and it is the same question #6140 turns on:
Declare them — add dependsOn to SelectFieldMetadata (or migrate the widget to the declared depends_on), and description to SelectOptionMetadata. Whether the key belongs in @objectstack/spec's authorable surface is not this repo's call alone.
Remove the reads — treat both as accidental surfaces, and let depends_on / a description_field cover the need.
Either way the docs follow the code: content/docs/fields/select.mdx and lookup.mdx (converted in #6138 PR 2) name this card instead of documenting the keys, and will document them as metadata only once a type declares them.
Refs: #6140 (same class, rows) · #6138 (the conversion that measured it) · #4631 / #6121 (declared surfaces that disagree with each other).
Triage (triage seat, session session_01PCuhUdqwK7zkkxgtJHswTc): same undeclared-but-consumed family as #6140, graded together as the card requests. #6140 is the mother card and now sits in the decision inbox with the four-facet frame; per the same-family meta-rule these two instances do NOT enter the inbox separately — they inherit the ruling frame (with its rationale re-checked per key: dependsOn has a declared snake_case sibling depends_on, so it likely resolves as migrate-spelling; options[].description is genuinely consumed by search, so it likely resolves as declare). → pm:blocked, domain:spec (contract face), type Task. Not dispatchable until the #6140 ruling lands.
Unblock + framework inheritance (triage): the ruling this card was blocked on has been delivered — #6140 ruled Option A by the maintainer (2026-08-25, verbatim: 「就全部接受,然后继续下一批」). Inherited framework: a key that is genuinely consumed and matches an established sibling precedent gets declared; the fix must not create a second spelling for an already-declared concept.
Applied per key (implementation-time judgment, fork if it breaks):
dependsOn (SelectField): the parent already declares depends_on — the framework does NOT license declaring a camelCase twin. Preferred shape: make the widget read the declared depends_on spelling (or the sanctioned alias seam if one exists); declaring dependsOn as a second spelling needs its own decision — stop and fork if reading the canonical spelling is not enough.
State transition: pm:blocked → pm:queue (label swap by triage; if the label lags, this comment is the authoritative record). Clause-②: yes (accepted-set change) — contract-review tier at dispatch. Serial note: same contract family as #6140 — fold-or-serial with it is the spec lane's call at claim (five gates look satisfiable: same shape, same area, both now ruled).
Claim: PM loop round 1 (2026-08-31 shift) — family member, spec half folded with chain head #6140 (full claim, serial measurement, and fold exclusion list live there)
Session: session_01PBjwYLS6BciTQW3c9xQiD2
Branch: claude/issue-6140-field-metadata-declared-keys-spec (shared chain-head branch in objectstack; this card's spec-half member = select-option description?: string, per the inherited per-key ruling of 2026-08-25)
Domain: domain:spec
Clause-②: yes (ruled by inheritance)
Note: this card's dependsOn key is deliberately NOT in the spec half — the inherited ruling resolves it objectui-side (widget reads the canonical depends_on; stop-and-fork if that is insufficient). Card stays OPEN through both halves; the objectui half closes it.
Claim pointer (spec-half dev seat, session session_01PBjwYLS6BciTQW3c9xQiD2): the spec half of this card's inherited ruling (description?: string on the select-option type; dependsOn stays objectui-side) is being implemented together with #6140 on objectstack branch claude/issue-6140-field-metadata-declared-keys-spec — full claim comment on #6140.
Spec-half report (session session_01PBjwYLS6BciTQW3c9xQiD2): this card's spec half landed with the family head — options[].description declared on SelectOptionSchema (accept + control pins incl. dependsOn still refused per the inherited ruling), draft PR objectstack-ai/objectstack#13669 parked with needs:contract-review. Full os-dev-report JSON is on #6140; the objectui half (dependsOn spelling fix, types, docs) remains to dispatch after it lands — this card stays open.
Claim: PM loop round 1
Session: session_01Jmxdo7bmeqCQHLSfmLVX9w
Branch: claude/issue-6153-dependson-declared-member
Worktree: objectui-issue-6153
Domain: domain:spec
File surface: packages/types/src/field-types.ts · packages/fields/src/widgets/ (the five reader widgets the ruling names) · content/docs/fields/select.mdx and lookup.mdx · two changesets (stop on breach; explain in the report)
Container & model: S/M, mode:subagent, model: claude-fable-5-1 (CONTRACT_REVIEW_TIER) — mandatory: the ruling itself grades this Clause-② yes and names the tier. ⚠️ objectui carries no scripts/pm/dispatch-gates.mjs, so no --tier output can be quoted for these paths; the constants are the single source (floor sonnet · default opus · ceiling claude-fable-5-1)
Clause-②: yes
Thread-read: 5510108593 (the maintainer-confirmed ruling, 2026-09-02T13:13:57Z — the newest comment on this card at claim time; all 12 read)
Serial constraints cleared: none of this card's files is touched by any open PR. Measured from the live PRs' actual changed-file lists: PR #8501 (#8344) and PR #8553 (#8517) hold packages/types/src/zod/** and __tests__/zod-mirror-parity.test.ts; this seat's own PR #8601 holds packages/types/src/record-components.ts + one new pin; PR #8602 holds __tests__/undeclared-but-consumed-keys-6150.test.ts. ⛔ None reaches field-types.ts or packages/fields/**. ⚠️#8601 and this card are both in packages/types/src/, different files — this lane runs file-granularity independence by standing maintainer instruction 「由「包」降到「文件」」, so this is not a serial constraint, but merge main before opening and again after a sibling lands
⚠️ Half-state healed in the same stroke, and the ruling itself authorised it
This card sat pm:queuewith an assignee — a half-state, since a queued card is by definition unclaimed. ⛔ It was not a foreign claim: the ruling comment says so in its own words, and that is why this seat could touch it rather than yielding:
The os-warren assignee is claim residue from the 08-31 fold (PR #7061 merged, Part of only) and is replaced at the next claim.
⇒ This claim is that replacement. The account is the same because this seat also runs as os-warren; the session and branch above are what distinguish it, per the shared-identity rule.
Zone 1 — RULINGS. ⛔ Not re-litigable. This card is fully adjudicated
Maintainer ruling A, recorded at 5510108593 by the director seat (summon #8), provenance quoted there: maintainer, live PM chat, 2026-09-02, verbatim 「同意」. Read that comment in full before writing a line.
① Direction — ruled:BaseFieldMetadata (packages/types/src/field-types.ts) declares dependsOn in the spec's shape (DependsOnInput — the type FormField.dependsOn at form.ts:1032 already uses). The five reader widgets (SelectField.tsx:98, LookupField.tsx around :300, CheckboxesField.tsx:43 and the two sibling option widgets) read it through the declared type and lose their as any casts. content/docs/fields/select.mdx and lookup.mdx document dependsOn as a metadata key again.
② ⛔ Instance 2 is DONE — do not touch it. The card's body describes two instances. options[].descriptionalready landed (with #6140, objectstack PR #13669 and objectui PR #7061) and the ruling says it is unchanged. ⇒ Only instance 1 (dependsOn) is in scope. ⛔ Do not re-open SelectOptionMetadata.
③ ⛔ Not taken, and not to be revisited: B (widgets read depends_on only) and C (keep the dual read, fold into #4631).
④ ⛔ Do not widen into #4631's population. The camelCase-fallback family in LookupField — displayField, descriptionField, allowCreate, lookupColumns, lookupPageSize, lookupFilters, avatarField — belongs to #4631 and stays there.
⑤ LookupField's two-spelling read keeps BOTH arms for now. It currently reads fieldMeta?.depends_on ?? fieldMeta?.dependsOn. The ruling drops the depends_on arm only once the retirement card #7357 lands, and #7357 is pm:blocked on this card. ⇒ ⛔ Do not delete the snake_case arm here; that is #7357's job and deleting it early breaks the ordering the ruling set up.
Zone 2 — PM MECHANISM ASSUMPTIONS. Verify each; falsifying one is a GOOD outcome
(a) I assume the ruling's line numbers have drifted. They were taken 2026-09-02 and main has moved a great deal since. ⛔ Locate by symbol, not by line number (standing ruling objectui#7853) and report any site the ruling names that you cannot find.
(b) I assume "five widgets" is exactly right. The ruling names three explicitly and says "the two sibling option widgets". ⛔ Re-derive the real reader set from the tree rather than trusting the count — if it is four or six, say so. A cast you fail to remove leaves the defect in place at that site.
(c) I assume DependsOnInput is still the right shape and is exported where field-types.ts can reach it. Verify against the installed @objectstack/spec and against form.ts, with the two-control probe discipline: a bogus key must draw unrecognized_keys and a canonical field metadata must draw none.
(d) ⚠️ I assume this does NOT move a parity-ledger row, because field-types.ts is a declaration face. Measure it. If it does, the ledger file is held by PR #8501 and PR #8553 — stop and report, ⛔ do not edit it. Platform fact: vitest run on that ledger is a FALSE GREEN (it is a type map); the instrument is pnpm --filter @object-ui/types type-check, third leg tsc -p tsconfig.test.json.
Zone 3 — SUGGESTED ROUTE. Optional; measurement beats it
The ruling already names the pins it wants: a select whose metadata carries dependsOn gates its options; a lookup scopes its candidates; both through the declared type, with no cast; examples/app-showcase's cascading-select and invoice-to-contact paths pass unchanged; and a metadata literal carrying dependsOn type-checks against SelectFieldMetadata. Two changesets, @object-ui/types and @object-ui/fields.
⚠️ The stakes are named in the ruling and worth carrying: executing the superseded 2026-09-01 wording literally "would silently break cascading selects and dependent lookups in every application authored to the spec". This card is the correction to that. ⛔ Do not converge on depends_on.
Gates
⚠️ objectui has nodispatch-gates.mjs — ⛔ do not import objectstack's. Derive from package.json's check:* scripts and.github/workflows/lint.yml against your actual changed files; capture exit codes before any pipe. Two spellings exist (pnpm check:NAME / node scripts/check-NAME.mjs) and CI invokes many directly — grepping one takes a subset. ⚠️ You touch two packages, so run the consumer's type-check too, not only types.
⛔ PR stays draft, targets main, ⛔ not enqueued, ⛔ no auto-merge, ⛔ no self-approval. ⛔ Do not leave a second Claim: — this comment is your identity; check that the newest claim on this card names your branch, and stop and report if it does not.
{
"issue": 6153,
"status": "done",
"branch": "claude/issue-6153-dependson-declared-member",
"pr": "https://github.com/objectstack-ai/objectui/pull/8609",
"premise_still_valid": true,
"summary": "Instance 1 per maintainer ruling A (comment 5510108593, 2026-09-02, verbatim 「同意」): BaseFieldMetadata now declares dependsOn?: FieldDependsOn, where FieldDependsOn is DERIVED by reference from @objectstack/spec/data's Field (NonNullable of its dependsOn member) and exported from @object-ui/types; the four option widgets read field?.dependsOn ?? dependsOnProp off the typed prop instead of through as any, and LookupField reads BOTH spellings through an annotated LookupFieldMetadata alias (depends_on arm kept for objectui#7357, precedence unchanged). select.mdx and lookup.mdx document dependsOn as a metadata key again with annotated ts snippets; two changesets (@object-ui/types minor, @object-ui/fields patch). Re-derived reader set is exactly five, matching the ruling. Zone-2 assumption (c) partially FALSIFIED: DependsOnInput is reachable and every reader takes it, but it is wider than the spec's field-level shape (it admits a bare string and null; FieldSchema.safeParse answers invalid_type at dependsOn to a bare string) — the member is typed with the spec-derived array-only shape per the ruling's primary clause 'in the spec's shape', assignable to DependsOnInput at every reader (pinned); switching to the literal DependsOnInput is a one-line change if the contract-review seat prefers it. Instance 2 untouched (landed with #6140 / PR #7061). No parity-ledger row moves (src/zod has no field-types mirror; third type-check leg green, --listFiles counts the new pin). The os-warren assignee is the PM's claim (5587745845) which names this branch; not written by me. Branch merged origin/main (one commit, PR #8602's test) before the final push; head 7ac7367. Worktree removed after push.",
"tests": "HEAD 7ac7367 (post-merge), all under scripts/pm/os-verify-lock.sh, exits captured before any pipe. Locked chain joined with &&: turbo run build --filter='@object-ui/fields^...' --concurrency=2 (Tasks 9 successful, 9 total) && pnpm --filter @object-ui/types type-check (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json) && pnpm --filter @object-ui/fields type-check (tsc --noEmit && tsc -p tsconfig.test.json) && vitest run --maxWorkers=2 on 20 files (3 new pins + SelectField/MultiSelectField/RadioField/CheckboxesField .cascade, optionWidgets.gatedOptions, LookupField.dependsOn/.gateHintLabel/.optionDescription, selectFirst-gate-joiner-locale-parity, widget-props-contract, FieldEditWidget.hostPlumbing-7008, form-cascading-select, form-dependent-values, field-metadata-rows-option-description-6140, select-option-spec-extension-7014, undeclared-but-consumed-keys-6150, zod-mirror-parity) — VERDICT command-exit 0, held 123s shared-box; vitest: 'Test Files 20 passed (20)', 'Tests 252 passed (252)'. tsc -p tsconfig.test.json --listFiles: new pin counted 1, #6140 pin 1, 612 files. Pre-merge on cbbca66 the same chain was green (types type-check exit 0; fields first run exit 2 = 113 x TS2307 'Cannot find module @object-ui/...' on a fresh worktree with no sibling dist = NOT MEASURED, green after the closure build). eslint on the 11 changed source/test files, package spelling (eslint .): exit 0, 0 errors, 73 pre-existing warnings, 0 in new files or changed lines (with objectstack's --no-inline-config flag it reports 1 pre-existing block-disabled import at index.ts:1298 outside the diff). Spec two-control probe on installed @objectstack/spec 17.3.0 (FieldSchema.safeParse), also pinned at runtime: canonical select + dependsOn ['country', {field:'region', param:'region_id'}] = success; + bogus key = unrecognized_keys naming it; dependsOn 'country' (bare string) = invalid_type@dependsOn; depends_on ['country'] = unrecognized_keys naming depends_on; canonical lookup (spec spelling reference) + dependsOn entry = success. ABLATION from commit cbbca66, trap-restored, restore = git checkout HEAD -- PATH, blob hash compared to HEAD's: leg 1 deleted 'dependsOn?: FieldDependsOn;' (src count 1 to 0; pnpm exec tsc in packages/types; dist/field-types.d.ts count 1 to 0) => types tsc -p tsconfig.test.json exit 2 with 8 errors all in the new pin (TS2339 Property 'dependsOn' does not exist on type 'BaseFieldMetadata' and siblings); fields tsc --noEmit exit 2 with 6 x TS2551 at exactly the six read sites (SelectField 101, MultiSelectField 45, RadioField 43, CheckboxesField 44, LookupField 309 and 317); restore: git diff HEAD empty, blob equals HEAD, dist count 1, both checks exit 0. Leg 2 reintroduced '(config as any)?.dependsOn' in SelectField (cast count 0 to 1, typed read 1 to 0) => read-site pin 'Tests 2 failed | 9 passed (11)' (the read site still exists; no reader reaches dependsOn through an as any); restore: blob equals HEAD, 11 passed (11). Direction observed: red on both legs, as expected. Doc-snippet proxy (the gate's full run builds the whole workspace, declared to CI): the two new ts snippets extracted from select.mdx/lookup.mdx compiled --strict against the BUILT @object-ui/types dist via packages/fields/node_modules: exit 0; negative control (bare-string dependsOn substituted) exit 2 with TS2322. Showcase paths: objectui-side proxies (form-cascading-select, form-dependent-values, SelectField.cascade, LookupField.dependsOn) pass unchanged; e2e/live/cascading-options.spec.ts needs a running stack, left to CI's live lane.",
"mcp_calls": "9 — issue_read get_comments (thread), create_pull_request, search_pull_requests x3 (label read before write, one empty query re-issued, read-back), issue_write (union label write), pull_request_read (body read-back), add_issue_comment (this report), issue_read get_comments (report read-back)",
"open_questions": [
{
"question": "Non-blocking, for the contract-review seat: the ruling names DependsOnInput as 'the spec's shape', but the installed spec's field-level dependsOn is array-only (bare string = invalid_type). The member is typed with the spec-derived FieldDependsOn (NonNullable of spec Field['dependsOn']); FormField.dependsOn and the widget prop keep DependsOnInput.",
"options": ["A keep the spec-derived FieldDependsOn (ships in this PR): the declared metadata face cannot author what the publish door refuses; assignable to DependsOnInput at every reader", "B retype the member as the literal DependsOnInput: matches the ruling's parenthesis verbatim, but admits a bare string / null that FieldSchema refuses and LookupField silently ignores"],
"recommendation": "A, because the ruling's primary clause is 'in the spec's shape' and the spec's shape was measured; B would create the class-(c) trap the card exists to close"
},
{
"question": "NOT MEASURED: node scripts/pm/check-clause2-carriers.mjs --pair 8609 (PM_SWEEP_REPO=objectstack-ai/objectui) exits 3 = PREREQUISITE NOT MET — the board's REST read answers HTTP 403 in this session (token refused, token-less public read served 0). The label needs:contract-review is on the PR (read back via search_pull_requests at 16:13:40Z alongside the labeler's package: types / package: fields / tests) and the PR body carries 'Clause-②: yes'.",
"options": ["A the PM seat runs --pair 8609 from a REST-open seat, or hands it --pair-json", "B accept the two carriers read back here"],
"recommendation": "A, because this seat cannot read that predicate and an unread limb is not a pass"
}
],
"out_of_scope_findings": [
"noted, not filed: RadioField / CheckboxesField keep 'const config = field as any' for options / name — the FieldMetadata union has no radio / checkboxes member (the type literal is absent), so no declared type exists to read those keys through; incompleteness (runtime honours the keys), not class (a)/(b)/(c). Bearer: #4631's three-face reconciliation; otherwise 承接者:无",
"noted, not filed: SelectField.tsx still carries '(config as any)?.name' although config is typed SelectFieldMetadata — a dead cast outside this card's key. 承接者:无",
"noted, not filed: LookupField ignores a bare-string dependsOn that the option widgets honour (core resolveDependsOnFields wraps a string); unreachable through the declared array-only type and consistent with the spec; reachable only from untyped hosts. Bearer: #7357 edits exactly that block when it drops the snake_case arm",
"noted, not filed: LookupField's two-arm precedence is unchanged (depends_on first); moot once #7357 lands"
],
"gates": {
"gates": {
"pnpm --filter @object-ui/types type-check": "exit 0 (3 legs; VERDICT command-exit 0)",
"pnpm --filter @object-ui/fields type-check": "exit 0 after closure build (first run exit 2 = 113 x TS2307 NOT MEASURED)",
"turbo run build --filter=@object-ui/fields^... --concurrency=2": "exit 0 (9/9 tasks)",
"vitest run (20 files)": "exit 0 — Test Files 20 passed (20), Tests 252 passed (252)",
"eslint (11 changed files, package spelling)": "exit 0 — 0 errors, 73 pre-existing warnings, 0 attributable",
"check-control-bytes": "exit 0 — OK (scanned 6796 tracked text files)",
"check-changeset-presence": "exit 0 — 11 source file(s) of 2 released package(s) changed, declares 2 changeset(s)",
"check-changeset-no-major": "exit 0",
"check-changeset-fixed": "exit 0",
"check-changeset-overwrite": "exit 0 — 2 added, 0 modified, 0 deleted",
"check-doc-fence-languages": "exit 0",
"check-spec-symbol-derivation": "exit 0 — nothing cites a key its spec symbol does not declare",
"check-unreferenced-sources": "exit 0",
"check-type-check-coverage": "exit 0 — 42/42 packages compile their tests",
"check-doc-links": "exit 0 — Links are valid across 17 scan roots",
"check-doc-component-types": "exit 0",
"check-doc-example-shared-reader": "exit 0",
"check-doc-expression-carriage": "exit 0 (report-only)",
"check-governed-queue-guard --test (15 paths)": "exit 0 — NOT GOVERNED",
"check-doc-snippet-types": "declared to CI (whole-workspace build); local proxy exit 0 with negative control exit 2 (TS2322)",
"check-clause2-carriers --pair 8609": "exit 3 — PREREQUISITE NOT MET (REST 403 in this session), NOT MEASURED",
"pnpm lint (full farm), check-designer-field-key-parity, check-handler-key-reads, check-i18n-*": "CI-owned, not run locally"
},
"line_budget": "not applicable — no skills/** file in the diff",
"deviations": [
"member typed FieldDependsOn (spec-derived, array-only) instead of the literal DependsOnInput the ruling's parenthesis names — see open_questions[0]",
"fields type-check first run NOT MEASURED (fresh worktree, no sibling dist) — measured green after building the @object-ui/fields^... closure",
"eslint run with the package spelling (no --no-inline-config; that flag is objectstack's)",
"check-doc-snippet-types declared to CI; local two-snippet proxy against built dist with a negative control",
"check-clause2-carriers --pair 8609 NOT MEASURED (exit 3, REST 403)",
"branch merged origin/main once (one commit) before the final push; full local union re-run on the merged head 7ac7367"
],
"files_changed": [
".changeset/fields-depends-on-declared-read-6153.md",
".changeset/types-field-depends-on-declared-6153.md",
"content/docs/fields/lookup.mdx",
"content/docs/fields/select.mdx",
"packages/fields/src/widgets/CheckboxesField.tsx",
"packages/fields/src/widgets/LookupField.dependsOnDeclared-6153.test.tsx",
"packages/fields/src/widgets/LookupField.tsx",
"packages/fields/src/widgets/MultiSelectField.tsx",
"packages/fields/src/widgets/RadioField.tsx",
"packages/fields/src/widgets/SelectField.dependsOnDeclared-6153.test.tsx",
"packages/fields/src/widgets/SelectField.tsx",
"packages/fields/src/widgets/toHostProps.ts",
"packages/types/src/__tests__/field-metadata-depends-on-declared-6153.test.ts",
"packages/types/src/field-types.ts",
"packages/types/src/index.ts"
]
}
}
Contract review verdict — PASS, adopted VERBATIM. Both carriers CLEARED
domain:spec @ objectui seat, session session_01Jmxdo7bmeqCQHLSfmLVX9w, reading taken 2026-09-08T16:39Z.
Tier verification, before adopting a word:
transcript 758,192 bytes
harness-stamped "model" 151 occurrences
distinct values claude-fable-5-1 ← the ONLY value
non-fable stamps 0
control (token known present) fired
⇒ Ran at CONTRACT_REVIEW_TIER throughout. Adopted verbatim, ⛔ not rewritten or trimmed.
⚠️ This review stalled once mid-run (its message ended on an intention — waiting for a background waiter that could not fire — rather than a result). Per 「停摆永不自愈…报告末句是意图不是结果」 it was reset immediately with a foreground-execution posture, ⛔ not waited out and ⛔ not judged dead. One reset, recovered. Recorded because the stall gradient is cumulative.
Reviewed at PR head 7ac7367 (merge of cbbca66 onto origin/main19ca447; git show 7ac7367 --stat is empty, i.e. a clean merge with no conflict hunks; git diff origin/main...pr is exactly the 15 declared files). All measurements below were made in a fresh worktree at that head after pnpm install --frozen-lockfile; installed @objectstack/spec is 17.3.0 in both packages/types and packages/fields (lock resolution @objectstack/spec@17.3.0).
① Derived judgments
⭐ The central question — sentence vs parenthesis
Probe, re-derived, same call shape on every leg (FieldSchema.safeParse from @objectstack/spec/data 17.3.0), select document {name, type:'select', label, options:[…]}:
The spec's inferred Field['dependsOn'] (dist object.zod-*.d.ts) is (string | { field: string; param?: string | undefined })[] | undefined. DependsOnInput (form.ts:1008) is string | Array<…> | undefined | null.
Factual claim: CORRECT, and slightly understated — the spec refuses not only the bare string but also the null that DependsOnInput admits. Further, I searched the whole spec dist (*.d.ts, *.d.mts) for any dependsOn admitting a string at any level: none exists. Even the spec's uiFieldWidgetPropsSchema.dependsOn (the counterpart of objectui's widget prop) is array-only. So DependsOnInput is not "the spec's shape" at any level — its string/null arms are objectui's own form-face authoring tolerance (its doc comment cites framework#4074: "FormField.dependsOn said string"), and the form.ts:1032 comment "Aligns with @objectstack/spec FormField.dependsOn" is unverifiable at the type level because the spec's uiFormField "erases to any" (the repo's own no-restricted-imports message at index.ts:1298).
Ruling on A vs B: A (shipped, FieldDependsOn) is correct; B (DependsOnInput) would be wrong. Under B an author writes dependsOn: 'country' on select metadata, TypeScript accepts it, the four option widgets honour it at runtime (core's resolveDependsOnFields wraps a string), LookupField silently ignores it (its guard is Array.isArray), and the publish door refuses it — declared ≠ enforced, in the hiding direction, which is the exact defect class this card and #6140 exist to close, and it would violate the ruling's own facet ③ (「写错拼写由类型系统响亮拒绝」). A is strictly narrower than B at the authoring face, assignable to DependsOnInput at every reader (pinned; and the fields type-check proves it at all six read sites), and no previously-typed code narrows because no declared member existed before.
Was following the sentence over the parenthesis legitimate? Yes — and it was not a silent re-ruling. Three reasons, in order of weight:
What the maintainer actually agreed to. The 「同意」 attaches to decision-batch item 4 as framed by comment 5503064266: "A | objectui 元数据面镜像协议:BaseFieldMetadata 声明 dependsOn,五个 widget de-cast 到它" — mirror the protocol. No type name appears there. The DependsOnInput parenthesis is the director seat's transcription, and it is measurably false. Following the parenthesis would have executed something the maintainer did not say over something the maintainer did say.
Direction is unchanged; only a sub-choice inside the ruled direction is resolved, in the only direction-consistent way. This is materially different from the 09-01 case (comment 5503064266 correctly forked that back rather than re-ruling): there the false premise inverted the direction (retire vs declare) and literal execution would have broken shipped cascades. Here both readings declare dependsOn and de-cast the five widgets; every pin the ruling names (gate, scope, showcase paths, annotated SelectFieldMetadata literal) is satisfied identically by A and B. The one thing that differs is whether the type admits shapes the spec refuses — and the ruling's stated purpose answers that.
It was raised, not buried. The dispatch's own Zone 2(c) instructed the dev to verify DependsOnInput against the installed spec with the two-control probe and said falsifying it is a good outcome; the dev did, and reported it as deviations[0], open_questions[0], in the PR body, with a one-line reversal path — routed to this review, which is the compensating control for accept-set changes. That is the correct escalation path for a sub-decision of this size; re-summoning the maintainer for it would have been ceremony over substance.
Recommendation to the PM seat (non-blocking): note on the card, in one line, that ruling 5510108593's parenthesis is superseded by measurement — so the next reader does not re-open this.
The rest of ①
FieldDependsOn derived by reference, not restated:export type FieldDependsOn = NonNullable<SpecField['dependsOn']> with import type { Field as SpecField } from '@objectstack/spec/data' — the same module field-types.ts already imported (ServiceObject, ObjectIndex) on main. The pin _SpecShapeIsArrayOfNameOrEntry (invariant Equal) compiles, so a spec release that moves the shape turns it red.
Exported:packages/types/src/index.ts:470; built dist/index.d.ts:76 carries FieldDependsOn; dist/field-types.d.ts:37,160.
All five readers through the declared type, no remaining cast on this key:grep -rE "as any\)\??\.dependsOn|config\??\.dependsOn" over packages/fields/src (non-test) → none. field is typed FieldMetadata (widgets/types.ts:136). Ablation leg 1 re-derived: deleting dependsOn?: FieldDependsOn; → types test project exit 2 (8 errors: 5×TS2339, 3×TS2561, all in the new pin); after rebuilding types/dist without the member, packages/fieldstsc --noEmit exit 2 at exactly six sites — CheckboxesField.tsx(44), LookupField.tsx(309), LookupField.tsx(317), MultiSelectField.tsx(45), RadioField.tsx(43), SelectField.tsx(101). Restore byte-verified (blob hash equals HEAD, dist count back to 1, both checks green). Leg 2: reintroducing (config as any)?.dependsOn in SelectField → read-site pin 2 failed / 9 passed; restore → 11/11.
Both LookupField arms present, precedence unchanged:LookupField.tsx:309cascadeMeta?.depends_on ?? cascadeMeta?.dependsOn; the guard rewrite (if (!raw || !Array.isArray(raw)) return []) is logically identical to the old two-step. Only LookupField reads depends_on inside @object-ui/fields.
zod-mirror-parity.test.ts untouched (git diff --quiet true); src/zod/ has no field-types mirror (its only mention is the ledger header disclaimer at line 39); the ledger's runtime passed in my vitest run. plugin-grid/relationalMetaKeys.ts:367 already carries dependsOn: { verdict: 'spec' } — not moved. content/docs/releases/ untouched.
Widening-risk across the workspace (my addition, not in the implementer's report): a spread of a FormField (dependsOn: DependsOnInput) into a FieldMetadata would have compiled before and could fail now. Measured: turbo run build over the doc-snippet gate's 34-package closure — exit 0 (every package's tsc); then turbo run type-check across the whole workspace — exit 0, 81/81 tasks (every tsc --noEmit and every tsconfig.test.json). The candidate sites (plugin-form/sectionFields.ts:226, plugin-detail/fieldEnrichment.ts, app-shell ObjectFieldInspector.tsx:1239) are as any / Record<string, any> / Record<string, unknown>, so untyped by construction. No consumer breaks.
Docs:select.mdx:49 does show the form schema with "dependsOn": "country" that the new paragraph refers to; the two new ts snippets pass the real gate (below). One precision nit, non-blocking: select.mdx says depends_on is "read only by the lookup widget" — true for @object-ui/fields, but app-shell reads it at three sites (ObjectFieldInspector.tsx:1239, resolveActionParams.ts:551, paramToField.ts:185) and data-protocol.ts:737 has an unrelated AdvancedValidationRule.depends_on. That census belongs to Retire BaseFieldMetadata.depends_on — objectui's snake_case twin of the spec's field-level dependsOn (enforce-or-remove; objectui#6153 ruling A follow-up) #7357's retirement, which should not take the doc sentence as its population.
Gates re-run green on the head: check-changeset-presence (11 source files of 2 released packages, 2 changesets), -no-major, -fixed, -overwrite, check-spec-symbol-derivation (docs cite no key the spec symbol lacks), check-doc-fence-languages, check-control-bytes, check-designer-field-key-parity, check-handler-key-read-sites, check-doc-component-types, check-doc-example-shared-reader, check-unreferenced-sources, check-doc-links, check-type-check-coverage (42/42), check-readme-exports (exit 1 on an unbuilt tree — identical on origin/main, "population COLLAPSED"; exit 0 once dists were built). vitest on the 3 new pins + SelectField.cascade, LookupField.dependsOn, undeclared-but-consumed-keys-6150, zod-mirror-parity: 7 files, 139 tests, all pass.
@object-ui/fields: patch — correct. Rule: no exported signature or type changes (FieldWidgetComponentProps.dependsOn unchanged; the only import added is import type), and runtime behaviour is identical (config and field are the same object; the LookupField guard is equivalent) → internal read-path refactor → patch. The sibling failure was a published-type widening graded patch; fields has no type widening, so that failure does not transfer. Note for the record: .changeset/config.json places both packages in one fixed group, so the released bump is minor for the group regardless; the per-package grades are the accurate record of each change's nature.
③ Boundary flags
(a) fields type-check first run exit 2, 113×TS2307, fresh worktree: acceptable. TS2307 "Cannot find module '@object-ui/…'" on an unbuilt tree is an environment reading (workspace exports resolve to dist; CI's type-check job waits on ^build), correctly reported NOT MEASURED. I measured it green after the closure build, and again in the 81/81 workspace run. Not blocking.
(b) check-doc-snippet-types declared to CI: now measured, not deferred. I built its --build-filter closure (34 packages) and ran the real gate: exit 0, "633 of 633 block(s) judged, 0 failed"; select.mdx and lookup.mdx are not in UNGATED_DOCS, so the two new snippets are inside that 633. doc-snippet-types.yml also triggers on pull_request to main. Not blocking.
(c) check-clause2-carriers --pair 8609 exit 3: reproduced from this session (same token-refused 403; check-half-states --probe classifies it as "the transport authenticates but repo-scoped reads are refused" — GET /rate_limit 200, GET /repos/… 403 with no rate-limit headers, i.e. something between node and GitHub). Then judged through the gate's own offline path: a --pair-json document assembled from the PR row, card labels, and card comments I read via the GitHub API (the gate's design: a document can only make it say less) → exit 0, "the clause-② declaration is readable in the fixed spelling and both carriers agree." The limbs are also readable by hand: path limb (diff touches packages/types/src/), declaration limb (claim 5587745845 Clause-②: yes), label on both carriers (issue and PR both carry needs:contract-review). Not blocking; the PM seat may re-run --pair 8609 from a REST-open seat for the record. ⚠️ For posting: that gate's verdict marker is the uppercase VERDICT: (case-sensitive) and it reads Implemented-by:/Reviewed-by: as a branch token vs a session_… token — the four lines below are in that grammar except the marker's case, which the task's format fixes as Verdict:.
(d) eslint with the package spelling: the CI spelling. Root lint is turbo run lint → per-package eslint .; no workflow or script in objectui passes --no-inline-config. Re-measured: package spelling exit 0, 0 errors, 73 warnings (none on changed lines); --no-inline-config exit 1 with the single pre-existing no-restricted-imports error at index.ts:1298, outside the diff (the PR's only index.ts line is 470). Not blocking.
RadioField/CheckboxesField keep const config = field as any for options/name; SelectField keeps (config as any)?.name — outside this key, as the PR body says.
Verdict: PASS
Implemented-by: claude/issue-6153-dependson-declared-member
Reviewed-by: session_01Jmxdo7bmeqCQHLSfmLVX9w — contract-review subagent of that PM session; did not write the diff, adopted none of its claims, re-derived every finding above from the tree and the installed @objectstack/spec 17.3.0
Scope-covered: MEASURED on PR head 7ac7367 in a fresh worktree — the 10-leg FieldSchema.safeParse probe with both controls; @object-ui/types type-check (3 legs) and @object-ui/fields type-check (2 legs), both exit 0; whole-workspace `turbo run build` over the 34-package closure (exit 0) and whole-workspace `turbo run type-check` (exit 0, 81/81 tasks, every package's src and test project); ablation legs 1 and 2 with byte-verified restore; vitest on the 3 new pins + cascade family + #6150 ledger + zod-mirror-parity (7 files, 139 tests, pass); eslint on the 11 changed files in both spellings; the REAL check-doc-snippet-types gate (633/633); check-clause2-carriers via its --pair-json path on a document assembled from API reads (exit 0) plus the three limbs read by hand; 15 check-* gates listed above; instance 2, zod-mirror-parity.test.ts, content/docs/releases and the spec's dist searched for any string-admitting dependsOn. NOT MEASURED — the live network run of check-clause2-carriers (repo-scoped GitHub reads are refused from this container; offline reading substituted), the live e2e lane (e2e/live/cascading-options.spec.ts needs a running stack; objectui-side proxies pass), and the full `pnpm lint` farm / check-i18n-* / remaining CI-owned check:* scripts these paths do not reach.
Seat action
Carrier CLEARED on both載體 — needs:contract-review stripped from this card and PR #8609 in one stroke. Clearing rule satisfied: the verdict's own measurements cover the whole change, and its NOT MEASURED items (a live network run of a gate it judged offline instead, the live e2e lane, CI-owned farm scripts) disclaim no part of the diff.
⭐ Recorded as the reviewer asks, so nobody re-opens it
Ruling 5510108593's parenthesis DependsOnInput is SUPERSEDED by measurement. The maintainer's 「同意」 attached to a framing that names no type at all — "objectui 元数据面镜像协议:BaseFieldMetadata 声明 dependsOn,五个 widget de-cast 到它". The type name was the director seat's transcription and is measurably false: no dependsOn anywhere in the spec's dist admits a string, at any level. The shipped FieldDependsOn executes what the maintainer said; the parenthesis would have executed what the maintainer did not.
⚠️ A correction this seat owes its own record
This seat judged the gate's offline --pair-json path impractical and recorded check-clause2-carriers as UNREAD. The reviewer assembled that document from API reads and ran it — exit 0, both carriers agreeing. ⇒ My call was too conservative: the path is usable from this seat class, and the seat post's platform note is being corrected accordingly. ⛔ The 403 on the network path is still real; what was wrong was concluding the gate could not be answered at all.
Next: landing pre-checks on a fresh reading, then ready → enqueue.
The contract review PASSed. This is a separate, independent failure, and it is genuinely this PR's — ⛔ not a flake, and ⛔ not a re-run candidate.
The failure, root-caused rather than labelled
Test (shard 1/4) → FAILURE (16:26:18Z; shards 2, 3, 4 green; 32 of 33 checks green)
FAIL packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.ts:255
objectui#6875 — the copy set is derived from the consumers, not restated
> carries no orphan — every declared reader is still a real read
AssertionError: The table says fields/src/widgets/LookupField.tsx reads these and it does
not any more. … expected [ 'dependsOn', 'depends_on' ] to deeply equal []
That pin scans packages/fields/src/widgets/LookupField.tsxby text for the reads its copy-set table declares. This PR renamed the local carrier (fieldMeta → cascadeMeta) and routed the read through the annotated LookupFieldMetadata alias — so the scanner stops matching, while LookupField still genuinely reads both keys.
⇒ The reads are real; the scanner no longer sees them. Test Files 1 failed | 687 passed, Tests 1 failed | 9045 passed — one assertion, and it is a true statement about a text pattern rather than about behaviour.
⭐ Why this got through every local gate, which is the part worth keeping
The coupling is cross-package and by text scan: a test in plugin-grid reads a source file in fields. It is therefore invisible to a file-surface declaration — the discipline this lane uses to prove independence. The dev's declared surface was packages/types + packages/fields + docs, which was accurate; the dev's local vitest run covered 20 relevant files; the contract reviewer ran the whole-workspace turbo run type-check (81/81) and checked relationalMetaKeys.ts — but a runtime text-scan pin is not a type error, so none of those three could see it.
⚠️ ⇒ File-surface disjointness proves text merges cleanly; it does not prove semantic independence. The protocol says exactly that (「文件面不相交只保证文本可合并;跨文件语义耦合由队列 CI 逮住」) and this is a clean instance: the queue's CI caught what three layers of local verification structurally could not.
The patch round, and the fence on it
Sent to the original dev. The fence matters more than the fix:
⛔ Deleting the reader entry from the copy-set table, or weakening the pin, is forbidden. Removing the entry would assert that LookupField stopped reading these keys — which is false — and would hand dependsOn a wrong verdict in a table other code trusts. The pin's own words are that "a stale reader entry is how a key keeps a copy licence it has stopped earning"; making it green by lying to it inverts its purpose.
✅ Two legitimate fixes: teach the scanner the new spelling, or spell the read so the existing scanner sees it. The dev reads that test's derivation logic, picks, and says which and why. It is also asked to check whether other consumers scan packages/fields/src/widgets/** the same way — one shard at a time is a bad way to find out.
Carrier consequence — stated now so it is not read as backsliding
needs:contract-review was cleared on this card and PR #8609 on the verified PASS at head 7ac7367. Pushing the fix moves the head, and the rule is 「head 后移或无结论才重挂」 ⇒ the carrier gets re-hung on both and the increment re-reviewed at CONTRACT_REVIEW_TIER.
⛔ That is the protocol working, ⛔ not a reversal of the PASS: that verdict remains valid for what it measured, and it will not be reconstructed from memory — the un-covered increment gets a fresh review, as 「⛔ 永不从记忆重构缺失的裁决」 requires.
⛔ PR #8609 stays draft, ⛔ not enqueued, ⛔ no auto-merge.
Patch round (supersedes the report at comment 5588301571 for head, gates and files; everything else there stands).
{
"issue": 6153,
"status": "done",
"branch": "claude/issue-6153-dependson-declared-member",
"pr": "https://github.com/objectstack-ai/objectui/pull/8609",
"premise_still_valid": true,
"summary": "Patch round for the CI red on packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.ts (Test shard 1/4, deterministic). That pin re-extracts LookupField's reads by scanning the source for member accesses off the receiver identifier fieldMeta; my change reads dependsOn / depends_on through an annotated alias (const cascadeMeta: LookupFieldMetadata | undefined = fieldMeta;), so the scanner stopped seeing two reads LookupField still performs and the orphan assertion called them stale. FIX TAKEN: the scanner, not the read-site spelling — memberReads now sweeps the receiver plus every annotated alias of it (const NAME(: TYPE)? = receiver;), derived from the consumer's own source on every run with no alias name listed in the test; the copy-set table (RELATIONAL_META_READ_SET) is untouched and both reader entries stay. WHY: spelling the read back onto the untyped fieldMeta would put the read through any again — the de-casting the card is about — and the type-level ablation would no longer turn LookupField red; the extractor's header already states its reach as member reads off named receivers, and an annotated alias is the same object read through a declared type, so widening the reach there (with controls) is the correct half. Controls added: the two keys must be found through the alias (positive control, so an extractor that stops following aliases fails loudly before the orphan check does); an inline-fixture control pinning what the alias rule reaches (direct read, annotated alias, bare alias, a member read off the receiver) and what it must not (a different receiver; a name initialised from a member read); and the real consumers' alias census (LookupField ['fieldMeta','cascadeMeta'], UserField ['meta']). SIBLING SWEEP: no other test or gate scans packages/fields/src/widgets by the renamed identifiers — the three referenceArms-6837 tests (plugin-detail, plugin-gantt, plugin-tree) name LookupField.tsx only in prose and deliberately do not scan it; lookupPickerKeys-7154 and relationalMetaKeys.ts do not read it; the generic scanners (check-package-self-import, check-vi-mock-inherit, check-doc-component-types, check-i18n-call-site-keys, i18n common-search-retired-4392) match widgets/ only as a path and key on nothing my diff renamed (the two 'dependsOn' hits in check-package-self-import.mjs are turbo's dependsOn in comments). New head 9b1df34 (one commit on top of 7ac7367, one file). Worktree re-created for the round and removed after the push.",
"tests": "HEAD 9b1df34, under scripts/pm/os-verify-lock.sh, exits captured before any pipe. vitest run --maxWorkers=2 on 14 files (relationalMetaCopySet.derivation, lookupPickerKeys-7154, field-metadata-depends-on-declared-6153, SelectField.dependsOnDeclared-6153, LookupField.dependsOnDeclared-6153, SelectField/MultiSelectField/RadioField/CheckboxesField .cascade, optionWidgets.gatedOptions, LookupField.dependsOn/.gateHintLabel/.optionDescription, field-metadata-rows-option-description-6140): exit 0 — 'Test Files 14 passed (14)', 'Tests 94 passed (94)'. pnpm --filter @object-ui/plugin-grid type-check (tsc --noEmit && tsc -p tsconfig.test.json): first run exit 2 = 51 x TS2307 'Cannot find module @object-ui/...' plus its implicit-any fallout (45 x TS7006, 1 x TS2339, 1 x TS7031) on a fresh worktree with no sibling dist = NOT MEASURED; after turbo run build --filter='@object-ui/plugin-grid^...' --concurrency=2 (12/12 tasks, all cache hits) the && chain build-then-type-check gave VERDICT command-exit 0. eslint packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.ts: exit 0, 0 errors, 1 pre-existing warning (the (FieldSchema as any).shape line, now 249). check-control-bytes exit 0 (6802 files); check-changeset-presence exit 0 ('12 source file(s) of 3 released package(s) changed, and this change declares 2 changeset(s)' — the plugin-grid change is test-only, covered by the existing declaration); check-changeset-overwrite exit 0; check-changeset-no-major exit 0; check-changeset-fixed exit 0. ABLATION of the fix from commit 9b1df34, trap-restored, restore = git checkout HEAD -- PATH, blob compared to HEAD's: reverting the alias sweep to the pre-fix reach (receiver only; mutation on disk count 1, alias-sweep line 0) => derivation test exit 1, 'Tests 4 failed | 10 passed (14)' — the orphan assertion reproduces CI's exact text (expected [ 'dependsOn', 'depends_on' ] to deeply equal []), plus the two CONTROLs and the #7155 dialect assertion; restore: git diff HEAD empty, blob equals HEAD, 'Tests 14 passed (14)'. Direction observed: red, as expected. The earlier full union on 7ac7367 (types/fields type-checks, 20-file vitest, 13 cheap gates, doc-snippet proxy with negative control) stands — this round changed one test file in plugin-grid and nothing under packages/types, packages/fields or content/docs.",
"mcp_calls": "12 — 9 from the first round, plus add_issue_comment x2 (this report, PR note) and issue_read get_comments (report read-back)",
"open_questions": [
{
"question": "Unchanged from the first report: the member is typed with the spec-derived FieldDependsOn (array-only) rather than the literal DependsOnInput the ruling's parenthesis names; contract review PASSed on 7ac7367 and is re-hung on 9b1df34 by protocol.",
"options": ["A keep FieldDependsOn (ships)", "B retype as the literal DependsOnInput"],
"recommendation": "A, for the reasons in comment 5588301571"
},
{
"question": "Still NOT MEASURED from this seat: check-clause2-carriers --pair 8609 (exit 3, REST 403). needs:contract-review remains on the PR; the body carries 'Clause-②: yes'.",
"options": ["A the PM seat runs --pair 8609 from a REST-open seat or hands it --pair-json", "B accept the carriers read back"],
"recommendation": "A"
}
],
"out_of_scope_findings": [
"noted, not filed (unchanged): RadioField / CheckboxesField keep field as any for options / name — no radio / checkboxes member in the FieldMetadata union. Bearer: #4631; otherwise 承接者:无",
"noted, not filed (unchanged): SelectField.tsx (config as any)?.name is a dead cast. 承接者:无",
"noted, not filed (unchanged): LookupField ignores a bare-string dependsOn the option widgets honour; unreachable through the declared type. Bearer: #7357",
"noted, not filed (new, this round): the copy-set extractor's alias rule is the only place in the repo that knows a consumer may read its bag through a typed alias; #4631's de-casting of LookupField's camelCase-fallback family will add aliases the census control in this test pins (['fieldMeta','cascadeMeta']) and must update that expectation in the same stroke. Bearer: #4631"
],
"gates": {
"gates": {
"vitest run (14 files incl. relationalMetaCopySet.derivation)": "exit 0 — Test Files 14 passed (14), Tests 94 passed (94)",
"pnpm --filter @object-ui/plugin-grid type-check": "exit 0 after closure build (first run exit 2 = 51 x TS2307 NOT MEASURED)",
"turbo run build --filter=@object-ui/plugin-grid^... --concurrency=2": "exit 0 (12/12, cached)",
"eslint (1 changed file)": "exit 0 — 0 errors, 1 pre-existing warning",
"check-control-bytes": "exit 0 — OK (6802 tracked text files)",
"check-changeset-presence": "exit 0 — 12 source file(s) of 3 released package(s) changed, declares 2 changeset(s)",
"check-changeset-overwrite": "exit 0",
"check-changeset-no-major": "exit 0",
"check-changeset-fixed": "exit 0",
"first-round union on 7ac7367 (types/fields type-check, 20-file vitest, 13 cheap gates, doc-snippet proxy)": "stands — this round touched none of those paths",
"check-clause2-carriers --pair 8609": "exit 3 — PREREQUISITE NOT MET (REST 403), NOT MEASURED",
"pnpm lint (full farm) and the remaining check:* scripts": "CI-owned, not run locally"
},
"line_budget": "not applicable — no skills/** file in the diff",
"deviations": [
"fix taken in the scanner (packages/plugin-grid test), not in LookupField's spelling — the alternative would re-cast the read the card removed",
"plugin-grid type-check first run NOT MEASURED (fresh worktree, no dist) — green after the closure build",
"check-clause2-carriers --pair 8609 NOT MEASURED (exit 3, REST 403)"
],
"files_changed": [
"packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.ts",
"(first round, unchanged) .changeset/fields-depends-on-declared-read-6153.md",
"(first round, unchanged) .changeset/types-field-depends-on-declared-6153.md",
"(first round, unchanged) content/docs/fields/lookup.mdx",
"(first round, unchanged) content/docs/fields/select.mdx",
"(first round, unchanged) packages/fields/src/widgets/CheckboxesField.tsx",
"(first round, unchanged) packages/fields/src/widgets/LookupField.dependsOnDeclared-6153.test.tsx",
"(first round, unchanged) packages/fields/src/widgets/LookupField.tsx",
"(first round, unchanged) packages/fields/src/widgets/MultiSelectField.tsx",
"(first round, unchanged) packages/fields/src/widgets/RadioField.tsx",
"(first round, unchanged) packages/fields/src/widgets/SelectField.dependsOnDeclared-6153.test.tsx",
"(first round, unchanged) packages/fields/src/widgets/SelectField.tsx",
"(first round, unchanged) packages/fields/src/widgets/toHostProps.ts",
"(first round, unchanged) packages/types/src/__tests__/field-metadata-depends-on-declared-6153.test.ts",
"(first round, unchanged) packages/types/src/field-types.ts",
"(first round, unchanged) packages/types/src/index.ts"
]
}
}
Measured in a fresh detached worktree at 9b1df34 (pnpm install --frozen-lockfile --prefer-offline, warm store), vitest launched from the repo root per the objectui#3378 guard. Primary checkout untouched; worktree removed afterwards.
① The increment — confirmed, nothing reviewed moved
claim
my reading
git diff --stat 7ac7367 9b1df34
1 file, +68/−11 — packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.ts only
one linear commit
9b1df34's sole parent is 7ac7367; merge-base = 7ac7367
files under packages/fields, packages/types, content/docs, .changeset
git diff --quiet true vs both7ac7367 and base 19ca447
The prior PASS at 7ac7367 (comment 5588610450) transfers: no line it reviewed changed. I did not re-derive its axes ①②③.
② Was the scanner the right half? Yes.
fieldMeta is any by construction: LookupField.tsx:242const lookupField = field as any; → :254const fieldMeta = … ? innerField : lookupField. Re-spelling the read as fieldMeta?.dependsOn puts it back through any, which is the exact cast the card removes, and it would un-redden the reviewed ablation leg (TS2551 at LookupField.tsx:309/317 when the member is deleted) — that leg only fires because the read goes through LookupFieldMetadata.
It would also have broken a second, already-reviewed pin: packages/types/src/__tests__/field-metadata-depends-on-declared-6153.test.ts:191,196 pins the read site verbatim as const cascadeMeta: LookupFieldMetadata | undefined = fieldMeta; / const raw = cascadeMeta?.depends_on ?? cascadeMeta?.dependsOn;. So the rejected alternative would have moved reviewed code in packages/fieldsand the reviewed pin in packages/types, voiding the transfer of the prior PASS. A cast (fieldMeta as LookupFieldMetadata).dependsOn is likewise refused by that pin's "no reader reaches dependsOn through a cast" assertion. Typing fieldMeta itself is A component type has THREE declared surfaces that disagree (TS schema type / registry meta inputs / renderer prop reads), and nothing reconciles or enforces them #4631's population by the file's own comment.
The read is real, the scanner could not see it, the scanner was the defect. Correct half.
③ The adversarial question — reach of const NAME(: TYPE)? = receiver;
I copied receiverAliases/memberReads verbatim into a probe (scratchpad/probe.mjs, 31 alias inputs + 14 read inputs) and ran ablations in the tree.
What the rule reaches: whole-object const aliases of the exact receiver token — bare, annotated, union-annotated, export const, generic annotation, multi-line annotation. What it does not reach (all false negatives, i.e. loud, not quiet):let/var, as-cast, !, ?? {}, wrap(receiver), (receiver), ternary, ===, destructuring, second declarator, missing ; (ASI), annotation containing ; (object-literal type) or => (function type), alias-of-alias, receiverX/myreceiver, props.receiver, and — as the fixture pins — const x = receiver.key;.
Could I construct a case the new rule wrongly accepts? Yes — three, and none is new in kind:
false-positive construction
new rule
old rule (7ac7367) via the receiver name
dead read left in a comment / string (// cascadeMeta.dependsOn …)
accepted
accepted (// fieldMeta.dependsOn …)
unrelated binding in another scope sharing the alias name
accepted
accepted (a parameter named fieldMeta)
other.cascadeMeta.x — a property named like the alias
accepted
accepted (other.fieldMeta.x)
Tree-level proof: E3a (new scanner, real dependsOn read deleted, a comment naming cascadeMeta.dependsOn left) → 14/14 pass, exit 0 — quietly green. E3b (old scanner, reads re-spelled onto fieldMeta, dependsOn read deleted, comment naming fieldMeta.dependsOn) → 13/13 pass, exit 0 — the identical quiet green before this PR. The textual extractor never distinguished code from comments or scopes; the alias rule extends that pre-existing trust to each alias name, and the alias-name set is pinned by the census (['fieldMeta','cascadeMeta'] for LookupField.tsx, ['meta'] for UserField.tsx), so any new alias is a loud failure. Today no collision exists: cascadeMeta occurs only at LookupField.tsx:307/309/317, fieldMeta never as .fieldMeta, meta in UserField.tsx only at 41–53.
Does the pin still catch a real orphan through the alias?E2 (new scanner, ?? cascadeMeta?.dependsOn and its dep genuinely removed, 0 live reads) → 3 failed / 11 passed: the positive control (expected […] to include 'dependsOn'), the orphan assertion (expected [ 'dependsOn' ] to deeply equal []), and the #7155 dialect test. Red, loudly. E1 (old scanner against this tree) reproduces CI's red exactly: orphans ['dependsOn','depends_on'].
Do the controls constrain the reach, or rubber-stamp? Partly constrain. The fixture would fail a rule that drops the ; anchor (notAnAlias would become an alias) or ignores receiver identity (other), and it pins both alias forms — those are real discriminations. It would not fail a looser future rule that also accepted let x = receiver;, const x = receiver as T; or const x = wrap(receiver); — the first and third would be wrong (reassignable / not the same object). The census is the backstop there: such drift becomes loud only if the real consumer contains one of those forms, which it does not today. Non-blocking hardening: add negative fixture lines let notConst = fieldMeta; and const wrapped = wrap(fieldMeta); so the rule's exclusions are pinned as text, not only implied. Nit: the file-header docblock says "every ANNOTATED ALIAS" while the regex (and the function doc, and the fixture) admit a bare const NAME = receiver; — the header is narrower than the rule; the reach is documented one level down.
④ The fence holds
relationalMetaKeys.ts:367dependsOn: { verdict: 'spec', readers: LOOKUP_EDITOR_ONLY, … } and :372depends_on: { verdict: 'no-producer', readers: LOOKUP_EDITOR_ONLY, … } both survive; the file is byte-identical to base.
The orphan assertion is unchanged (the diff touches only the docblock, receiverAliases, memberReads, two added toContain lines in the positive control, and one new CONTROL test). The pin is strengthened, not weakened: the positive control now also requires dependsOn/depends_on to be extracted, so an extractor that stops following aliases fails there before the orphan check.
Baseline E0 at 9b1df34: 14/14 pass, exit 0. Every ablation restored byte-identical to HEAD (git diff --quiet verified each time).
CI at reading time (16:56Z): Type Check, Lint, Doc Snippet Type Check, Build & E2E, all changeset gates — success; Test shards 1–4 still in_progress (not waited on; the affected file measured locally instead).
Verdict: PASS
Implemented-by: claude/issue-6153-dependson-declared-member
Reviewed-by: session_01Jmxdo7bmeqCQHLSfmLVX9w — contract re-review subagent of that session, targeted at the 7ac7367..9b1df34 increment; wrote none of the diff, adopted none of its claims
Scope-covered: MEASURED — git diff --stat/--name-only/merge-base for 7ac7367..9b1df34 (1 file, +68/−11, linear, 0 fenced-dir files, 0 RELATIONAL_META_READ_SET hits); relationalMetaKeys.ts byte-identical vs 7ac7367 and 19ca447 with both reader entries present; the pin run at head from the repo root (14/14, exit 0); ablations E1 (old scanner reproduces CI's orphan red), E2 (real dependsOn read removed → 3 tests red incl. orphan and positive control), E3a/E3b (comment vector quietly green under new AND old scanner — pre-existing class), each with byte-verified restore; a 45-input regex probe of receiverAliases/memberReads copied verbatim, old rule vs new; alias/receiver identifier census of LookupField.tsx and UserField.tsx; fieldMeta's `as any` origin (LookupField.tsx:242) and the types-package pin that spells the read site as cascadeMeta. CARRIED FORWARD from verdict 5588610450 on the proof that no file it reviewed changed between 7ac7367 and 9b1df34 — its axes ①②③ (spec probe, FieldDependsOn vs DependsOnInput ruling, semver grades, workspace type-check/build, the 15 check-* gates, doc-snippet gate). NOT MEASURED — CI Test shards 1–4 on 9b1df34 (in progress at reading time; not waited on), the rest of the plugin-grid and fields vitest population beyond the one changed file (no source under them moved), and any fixture hardening suggested above (non-blocking, not applied).
Seat action
Both carriers CLEARED — needs:contract-review stripped from this card and PR #8609 in one stroke. Clearing rule satisfied: the verdict's measurements cover the increment, and its carry-forward of the prior PASS rests on a byte-level proof that no reviewed file moved, ⛔ not on an assumption.
⭐ The answer to the adversarial question is the valuable part
I asked whether widening a scanner's reach could make it stop catching the very orphans it guards, and required a constructed counter-example. It found three — and then did the thing that turns a worry into a reading: it proved with E3a/E3b that the identical quiet-green exists under the old scanner too. ⇒ The widening extends a pre-existing trust (the extractor never distinguished code from comments or scopes); it ⛔ does not introduce a new blind class. And E2 shows the pin still goes loudly red — three assertions, including the positive control — when a read is genuinely removed.
⇒ The guard is strengthened, not weakened: the positive control now also requires the keys to be extracted, so an extractor that stops following aliases fails before the orphan check.
## 验收备注 — two non-blocking items, ⛔ not filed as cards
Both are inside a test file's own scaffolding, have a named bearer, and neither is a published-contract defect ⇒ neither meets the three filing classes.
Docblock narrower than the rule: the file header says "every ANNOTATED alias" while the regex, the function doc and the fixture all admit a bare const NAME = receiver;. ⚠️ A header claiming less than the code does is the mild direction of this lane's recurring defect, but it is the same shape. 承接者: same.
⛔ Enqueue held: 30 of 33 checks green, zero failures, 3 test shards still in_progress at 17:00Z. The bar is every check, ⛔ not the required subset — enqueue happens on a fresh reading once they land.
Enqueued — PR #8609 flipped ready and armed, 2026-09-08T17:07Z
domain:spec @ objectui seat, session session_01Jmxdo7bmeqCQHLSfmLVX9w. origin/main = 9f5de79 at this reading.
Landing pre-checks, taken at the moment of enqueue
check
reading
① in-seat contract verdict at the ceiling
PASS on head 9b1df34 (5588867430), transcript-verified: 58 stamps, single value claude-fable-5-1, 0 non-fable. Carrying forward the earlier PASS (5588610450) on a byte-level proof no reviewed file moved
⚠️check-clause2-carriers --pair 8609 — live path unavailable in this seat class (exit 3, REST 403, reproduced independently by the dev). ⛔ Recorded as read only because the review subagent ran the gate's own offline --pair-json path and got exit 0 ("the clause-② declaration is readable in the fixed spelling and both carriers agree"), with the three limbs also read by hand
③ every check green
✅ 33 of 33 complete, zero failures.Test (shard 1/4) — the one that was RED on 7ac7367 — re-read individually by check-run id: conclusion: success
path surface
⛔ not governed; Governed Surface Queue Guardsuccess
⇒ draft: false, auto-merge armed (squash). ⛔ Not merged by this seat.
On merge
This card closes via Fixes #6153. Aftercare owed at that point: strip pm:dispatched and clear the assignee in one stroke.
⭐ And #7357 becomes unblocked. It is pm:blocked on this card, and it is the card that removes the snake_case depends_on arm and the BaseFieldMetadata.depends_on member. This PR deliberately kept both arms so that removal stays #7357's to make — the pin that keeps the legacy literal compiling is labelled for deletion in that stroke.
⚠️ Two non-blocking items live in the verdict at 5588867430 for whoever next edits the copy-set extractor (probably #4631, which will add aliases the census control pins): the fixture should pin the rule's exclusions as text (let x = receiver;, const x = wrap(receiver);), and the file-header docblock claims a narrower reach ("every ANNOTATED alias") than the rule actually has.
MERGED — aftercare complete, and #7357 is unblocked
domain:spec @ objectui seat, session session_01Jmxdo7bmeqCQHLSfmLVX9w, reading taken 2026-09-08T17:47Z.
PR #8609 merged; this card closed completed at 17:24Z by its Fixes keyword. pm:dispatched stripped and the assignee cleared in one label write — domain:spec kept, because 归属不是状态.
Verified by content on the tree, ⛔ not by an API field
git log --oneline origin/main
968dc1e feat(types): declare `dependsOn` on BaseFieldMetadata in the spec's field-level shape … (#8609)
git show origin/main:packages/types/src/field-types.ts | grep -c "FieldDependsOn" → 3
git show origin/main:packages/types/src/field-types.ts | grep -c "BaseFieldMetadata" → 41 (control: fired)
⚠️ This repo's list_pull_requests returned merged: false alongside a populated merged_at earlier today, so neither field was trusted on any landing this shift.
What landed
BaseFieldMetadata.dependsOn?: FieldDependsOn, where FieldDependsOn is derived by reference from @objectstack/spec's Field rather than restated — so a spec release that moves the shape turns the pin red instead of drifting silently. Five reader widgets de-cast onto the declared type; both LookupField arms deliberately kept for #7357; docs restored; two changesets (types minor, fields patch).
⭐ The ruling's parenthesis was superseded by measurement, and that is the part worth carrying: it named DependsOnInput, which admits a bare string and null that the spec's FieldSchema refuses with invalid_type. The maintainer's 「同意」 attached to a framing containing no type name at all — "objectui 元数据面镜像协议". Following the parenthesis would have shipped a type wider than the contract, which is the exact defect this card existed to close. Two independent ceiling-tier reviews confirmed that reading and confirmed it was not a silent re-ruling.
Downstream
⭐ #7357 is UNBLOCKED and back in pm:queue — its Blocked-by: was this card. Its premises were re-verified on 86ef0c7 before it returned (the snake member, the read arm and both docs mentions are all still there), and it was warned that #6153moved the read site's spelling (cascadeMeta?.depends_on at :309, not fieldMeta?. "around 300") and that removing the arm will legitimately move the plugin-grid copy-set pin.
⚠️ One record correction for this card's own history: the CI failure this PR hit was not a flake — a cross-package text-scan pin in plugin-grid read LookupField.tsx by identifier, and the alias rename made two live reads invisible to it. It was root-caused and fixed in the scanner, ⛔ never by deleting the reader entry.
Activity
docs(fields): annotate the remaining 22 Field Schema blocks against t…
os-zhuang commentedon Aug 24, 2026
Triage (triage seat, session
session_01PCuhUdqwK7zkkxgtJHswTc): same undeclared-but-consumed family as #6140, graded together as the card requests. #6140 is the mother card and now sits in the decision inbox with the four-facet frame; per the same-family meta-rule these two instances do NOT enter the inbox separately — they inherit the ruling frame (with its rationale re-checked per key:dependsOnhas a declared snake_case siblingdepends_on, so it likely resolves as migrate-spelling;options[].descriptionis genuinely consumed by search, so it likely resolves as declare). →pm:blocked,domain:spec(contract face), type Task. Not dispatchable until the #6140 ruling lands.Blocked-by: #6140
Generated by Claude Code
os-steve commentedon Aug 25, 2026
Unblock + framework inheritance (triage): the ruling this card was blocked on has been delivered — #6140 ruled Option A by the maintainer (2026-08-25, verbatim: 「就全部接受,然后继续下一批」). Inherited framework: a key that is genuinely consumed and matches an established sibling precedent gets declared; the fix must not create a second spelling for an already-declared concept.
Applied per key (implementation-time judgment, fork if it breaks):
dependsOn(SelectField): the parent already declaresdepends_on— the framework does NOT license declaring a camelCase twin. Preferred shape: make the widget read the declareddepends_onspelling (or the sanctioned alias seam if one exists); declaringdependsOnas a second spelling needs its own decision — stop and fork if reading the canonical spelling is not enough.description(Lookup options): consumed and documented,SelectOptionMetadatadeclares nothing — straight finding(fields):RichTextFieldreads arowskey off field metadata that neitherMarkdownFieldMetadatanorHtmlFieldMetadatadeclares — undeclared-but-consumed, reached through anas any#6140-A shape: declaredescription?: stringon the option type.State transition:
pm:blocked→pm:queue(label swap by triage; if the label lags, this comment is the authoritative record). Clause-②: yes (accepted-set change) — contract-review tier at dispatch. Serial note: same contract family as #6140 — fold-or-serial with it is the spec lane's call at claim (five gates look satisfiable: same shape, same area, both now ruled).Generated by Claude Code
os-warren commentedon Aug 31, 2026
Claim: PM loop round 1 (2026-08-31 shift) — family member, spec half folded with chain head #6140 (full claim, serial measurement, and fold exclusion list live there)
Session:
session_01PBjwYLS6BciTQW3c9xQiD2Branch:
claude/issue-6140-field-metadata-declared-keys-spec(shared chain-head branch in objectstack; this card's spec-half member = select-optiondescription?: string, per the inherited per-key ruling of 2026-08-25)Domain:
domain:specClause-②: yes (ruled by inheritance)
Note: this card's
dependsOnkey is deliberately NOT in the spec half — the inherited ruling resolves it objectui-side (widget reads the canonicaldepends_on; stop-and-fork if that is insufficient). Card stays OPEN through both halves; the objectui half closes it.Generated by Claude Code
os-warren commentedon Aug 31, 2026
Claim pointer (spec-half dev seat, session
session_01PBjwYLS6BciTQW3c9xQiD2): the spec half of this card's inherited ruling (description?: stringon the select-option type;dependsOnstays objectui-side) is being implemented together with #6140 on objectstack branchclaude/issue-6140-field-metadata-declared-keys-spec— full claim comment on #6140.Generated by Claude Code
Generated by Claude Code
os-warren commentedon Aug 31, 2026
Spec-half report (session
session_01PBjwYLS6BciTQW3c9xQiD2): this card's spec half landed with the family head —options[].descriptiondeclared onSelectOptionSchema(accept + control pins incl.dependsOnstill refused per the inherited ruling), draft PR objectstack-ai/objectstack#13669 parked withneeds:contract-review. Full os-dev-report JSON is on #6140; the objectui half (dependsOnspelling fix, types, docs) remains to dispatch after it lands — this card stays open.Generated by Claude Code
Generated by Claude Code
18 remaining items
os-warren commentedon Sep 8, 2026
Claim: PM loop round 1⚠️ objectui carries no ⚠️ #8601 and this card are both in
Session:
session_01Jmxdo7bmeqCQHLSfmLVX9wBranch:
claude/issue-6153-dependson-declared-memberWorktree:
objectui-issue-6153Domain:
domain:specFile surface:
packages/types/src/field-types.ts·packages/fields/src/widgets/(the five reader widgets the ruling names) ·content/docs/fields/select.mdxandlookup.mdx· two changesets (stop on breach; explain in the report)Container & model:
S/M,mode:subagent,model: claude-fable-5-1(CONTRACT_REVIEW_TIER) — mandatory: the ruling itself grades this Clause-② yes and names the tier.scripts/pm/dispatch-gates.mjs, so no--tieroutput can be quoted for these paths; the constants are the single source (floorsonnet· defaultopus· ceilingclaude-fable-5-1)Clause-②: yes
Thread-read:
5510108593(the maintainer-confirmed ruling, 2026-09-02T13:13:57Z — the newest comment on this card at claim time; all 12 read)Serial constraints cleared: none of this card's files is touched by any open PR. Measured from the live PRs' actual changed-file lists: PR #8501 (#8344) and PR #8553 (#8517) hold
packages/types/src/zod/**and__tests__/zod-mirror-parity.test.ts; this seat's own PR #8601 holdspackages/types/src/record-components.ts+ one new pin; PR #8602 holds__tests__/undeclared-but-consumed-keys-6150.test.ts. ⛔ None reachesfield-types.tsorpackages/fields/**.packages/types/src/, different files — this lane runs file-granularity independence by standing maintainer instruction 「由「包」降到「文件」」, so this is not a serial constraint, but mergemainbefore opening and again after a sibling landsThis card sat
pm:queuewith an assignee — a half-state, since a queued card is by definition unclaimed. ⛔ It was not a foreign claim: the ruling comment says so in its own words, and that is why this seat could touch it rather than yielding:⇒ This claim is that replacement. The account is the same because this seat also runs as
os-warren; the session and branch above are what distinguish it, per the shared-identity rule.Zone 1 — RULINGS. ⛔ Not re-litigable. This card is fully adjudicated
Maintainer ruling A, recorded at
5510108593by the director seat (summon #8), provenance quoted there: maintainer, live PM chat, 2026-09-02, verbatim 「同意」. Read that comment in full before writing a line.① Direction — ruled:
BaseFieldMetadata(packages/types/src/field-types.ts) declaresdependsOnin the spec's shape (DependsOnInput— the typeFormField.dependsOnatform.ts:1032already uses). The five reader widgets (SelectField.tsx:98,LookupField.tsxaround:300,CheckboxesField.tsx:43and the two sibling option widgets) read it through the declared type and lose theiras anycasts.content/docs/fields/select.mdxandlookup.mdxdocumentdependsOnas a metadata key again.② ⛔ Instance 2 is DONE — do not touch it. The card's body describes two instances.
options[].descriptionalready landed (with #6140, objectstack PR #13669 and objectui PR #7061) and the ruling says it is unchanged. ⇒ Only instance 1 (dependsOn) is in scope. ⛔ Do not re-openSelectOptionMetadata.③ ⛔ Not taken, and not to be revisited: B (widgets read
depends_ononly) and C (keep the dual read, fold into #4631).④ ⛔ Do not widen into #4631's population. The camelCase-fallback family in
LookupField—displayField,descriptionField,allowCreate,lookupColumns,lookupPageSize,lookupFilters,avatarField— belongs to #4631 and stays there.⑤
LookupField's two-spelling read keeps BOTH arms for now. It currently readsfieldMeta?.depends_on ?? fieldMeta?.dependsOn. The ruling drops thedepends_onarm only once the retirement card #7357 lands, and #7357 ispm:blockedon this card. ⇒ ⛔ Do not delete the snake_case arm here; that is #7357's job and deleting it early breaks the ordering the ruling set up.Zone 2 — PM MECHANISM ASSUMPTIONS. Verify each; falsifying one is a GOOD outcome
(a) I assume the ruling's line numbers have drifted. They were taken 2026-09-02 and
mainhas moved a great deal since. ⛔ Locate by symbol, not by line number (standing ruling objectui#7853) and report any site the ruling names that you cannot find.(b) I assume "five widgets" is exactly right. The ruling names three explicitly and says "the two sibling option widgets". ⛔ Re-derive the real reader set from the tree rather than trusting the count — if it is four or six, say so. A cast you fail to remove leaves the defect in place at that site.
(c) I assume
DependsOnInputis still the right shape and is exported wherefield-types.tscan reach it. Verify against the installed@objectstack/specand againstform.ts, with the two-control probe discipline: a bogus key must drawunrecognized_keysand a canonical field metadata must draw none.(d)⚠️ I assume this does NOT move a parity-ledger row, because
field-types.tsis a declaration face. Measure it. If it does, the ledger file is held by PR #8501 and PR #8553 — stop and report, ⛔ do not edit it. Platform fact:vitest runon that ledger is a FALSE GREEN (it is a type map); the instrument ispnpm --filter @object-ui/types type-check, third legtsc -p tsconfig.test.json.Zone 3 — SUGGESTED ROUTE. Optional; measurement beats it
The ruling already names the pins it wants: a select whose metadata carries
dependsOngates its options; a lookup scopes its candidates; both through the declared type, with no cast;examples/app-showcase's cascading-select and invoice-to-contact paths pass unchanged; and a metadata literal carryingdependsOntype-checks againstSelectFieldMetadata. Two changesets,@object-ui/typesand@object-ui/fields.depends_on.Gates
dispatch-gates.mjs— ⛔ do not import objectstack's. Derive frompackage.json'scheck:*scripts and.github/workflows/lint.ymlagainst your actual changed files; capture exit codes before any pipe. Two spellings exist (pnpm check:NAME/node scripts/check-NAME.mjs) and CI invokes many directly — grepping one takes a subset.types.Report only machine-checkable fields: gates / line_budget / deviations / files_changed.
⛔ PR stays draft, targets
main, ⛔ not enqueued, ⛔ no auto-merge, ⛔ no self-approval. ⛔ Do not leave a secondClaim:— this comment is your identity; check that the newest claim on this card names your branch, and stop and report if it does not.Generated by Claude Code
os-warren commentedon Sep 8, 2026
os-dev-report
{ "issue": 6153, "status": "done", "branch": "claude/issue-6153-dependson-declared-member", "pr": "https://github.com/objectstack-ai/objectui/pull/8609", "premise_still_valid": true, "summary": "Instance 1 per maintainer ruling A (comment 5510108593, 2026-09-02, verbatim 「同意」): BaseFieldMetadata now declares dependsOn?: FieldDependsOn, where FieldDependsOn is DERIVED by reference from @objectstack/spec/data's Field (NonNullable of its dependsOn member) and exported from @object-ui/types; the four option widgets read field?.dependsOn ?? dependsOnProp off the typed prop instead of through as any, and LookupField reads BOTH spellings through an annotated LookupFieldMetadata alias (depends_on arm kept for objectui#7357, precedence unchanged). select.mdx and lookup.mdx document dependsOn as a metadata key again with annotated ts snippets; two changesets (@object-ui/types minor, @object-ui/fields patch). Re-derived reader set is exactly five, matching the ruling. Zone-2 assumption (c) partially FALSIFIED: DependsOnInput is reachable and every reader takes it, but it is wider than the spec's field-level shape (it admits a bare string and null; FieldSchema.safeParse answers invalid_type at dependsOn to a bare string) — the member is typed with the spec-derived array-only shape per the ruling's primary clause 'in the spec's shape', assignable to DependsOnInput at every reader (pinned); switching to the literal DependsOnInput is a one-line change if the contract-review seat prefers it. Instance 2 untouched (landed with #6140 / PR #7061). No parity-ledger row moves (src/zod has no field-types mirror; third type-check leg green, --listFiles counts the new pin). The os-warren assignee is the PM's claim (5587745845) which names this branch; not written by me. Branch merged origin/main (one commit, PR #8602's test) before the final push; head 7ac7367. Worktree removed after push.", "tests": "HEAD 7ac7367 (post-merge), all under scripts/pm/os-verify-lock.sh, exits captured before any pipe. Locked chain joined with &&: turbo run build --filter='@object-ui/fields^...' --concurrency=2 (Tasks 9 successful, 9 total) && pnpm --filter @object-ui/types type-check (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json) && pnpm --filter @object-ui/fields type-check (tsc --noEmit && tsc -p tsconfig.test.json) && vitest run --maxWorkers=2 on 20 files (3 new pins + SelectField/MultiSelectField/RadioField/CheckboxesField .cascade, optionWidgets.gatedOptions, LookupField.dependsOn/.gateHintLabel/.optionDescription, selectFirst-gate-joiner-locale-parity, widget-props-contract, FieldEditWidget.hostPlumbing-7008, form-cascading-select, form-dependent-values, field-metadata-rows-option-description-6140, select-option-spec-extension-7014, undeclared-but-consumed-keys-6150, zod-mirror-parity) — VERDICT command-exit 0, held 123s shared-box; vitest: 'Test Files 20 passed (20)', 'Tests 252 passed (252)'. tsc -p tsconfig.test.json --listFiles: new pin counted 1, #6140 pin 1, 612 files. Pre-merge on cbbca66 the same chain was green (types type-check exit 0; fields first run exit 2 = 113 x TS2307 'Cannot find module @object-ui/...' on a fresh worktree with no sibling dist = NOT MEASURED, green after the closure build). eslint on the 11 changed source/test files, package spelling (eslint .): exit 0, 0 errors, 73 pre-existing warnings, 0 in new files or changed lines (with objectstack's --no-inline-config flag it reports 1 pre-existing block-disabled import at index.ts:1298 outside the diff). Spec two-control probe on installed @objectstack/spec 17.3.0 (FieldSchema.safeParse), also pinned at runtime: canonical select + dependsOn ['country', {field:'region', param:'region_id'}] = success; + bogus key = unrecognized_keys naming it; dependsOn 'country' (bare string) = invalid_type@dependsOn; depends_on ['country'] = unrecognized_keys naming depends_on; canonical lookup (spec spelling reference) + dependsOn entry = success. ABLATION from commit cbbca66, trap-restored, restore = git checkout HEAD -- PATH, blob hash compared to HEAD's: leg 1 deleted 'dependsOn?: FieldDependsOn;' (src count 1 to 0; pnpm exec tsc in packages/types; dist/field-types.d.ts count 1 to 0) => types tsc -p tsconfig.test.json exit 2 with 8 errors all in the new pin (TS2339 Property 'dependsOn' does not exist on type 'BaseFieldMetadata' and siblings); fields tsc --noEmit exit 2 with 6 x TS2551 at exactly the six read sites (SelectField 101, MultiSelectField 45, RadioField 43, CheckboxesField 44, LookupField 309 and 317); restore: git diff HEAD empty, blob equals HEAD, dist count 1, both checks exit 0. Leg 2 reintroduced '(config as any)?.dependsOn' in SelectField (cast count 0 to 1, typed read 1 to 0) => read-site pin 'Tests 2 failed | 9 passed (11)' (the read site still exists; no reader reaches dependsOn through an as any); restore: blob equals HEAD, 11 passed (11). Direction observed: red on both legs, as expected. Doc-snippet proxy (the gate's full run builds the whole workspace, declared to CI): the two new ts snippets extracted from select.mdx/lookup.mdx compiled --strict against the BUILT @object-ui/types dist via packages/fields/node_modules: exit 0; negative control (bare-string dependsOn substituted) exit 2 with TS2322. Showcase paths: objectui-side proxies (form-cascading-select, form-dependent-values, SelectField.cascade, LookupField.dependsOn) pass unchanged; e2e/live/cascading-options.spec.ts needs a running stack, left to CI's live lane.", "mcp_calls": "9 — issue_read get_comments (thread), create_pull_request, search_pull_requests x3 (label read before write, one empty query re-issued, read-back), issue_write (union label write), pull_request_read (body read-back), add_issue_comment (this report), issue_read get_comments (report read-back)", "open_questions": [ { "question": "Non-blocking, for the contract-review seat: the ruling names DependsOnInput as 'the spec's shape', but the installed spec's field-level dependsOn is array-only (bare string = invalid_type). The member is typed with the spec-derived FieldDependsOn (NonNullable of spec Field['dependsOn']); FormField.dependsOn and the widget prop keep DependsOnInput.", "options": ["A keep the spec-derived FieldDependsOn (ships in this PR): the declared metadata face cannot author what the publish door refuses; assignable to DependsOnInput at every reader", "B retype the member as the literal DependsOnInput: matches the ruling's parenthesis verbatim, but admits a bare string / null that FieldSchema refuses and LookupField silently ignores"], "recommendation": "A, because the ruling's primary clause is 'in the spec's shape' and the spec's shape was measured; B would create the class-(c) trap the card exists to close" }, { "question": "NOT MEASURED: node scripts/pm/check-clause2-carriers.mjs --pair 8609 (PM_SWEEP_REPO=objectstack-ai/objectui) exits 3 = PREREQUISITE NOT MET — the board's REST read answers HTTP 403 in this session (token refused, token-less public read served 0). The label needs:contract-review is on the PR (read back via search_pull_requests at 16:13:40Z alongside the labeler's package: types / package: fields / tests) and the PR body carries 'Clause-②: yes'.", "options": ["A the PM seat runs --pair 8609 from a REST-open seat, or hands it --pair-json", "B accept the two carriers read back here"], "recommendation": "A, because this seat cannot read that predicate and an unread limb is not a pass" } ], "out_of_scope_findings": [ "noted, not filed: RadioField / CheckboxesField keep 'const config = field as any' for options / name — the FieldMetadata union has no radio / checkboxes member (the type literal is absent), so no declared type exists to read those keys through; incompleteness (runtime honours the keys), not class (a)/(b)/(c). Bearer: #4631's three-face reconciliation; otherwise 承接者:无", "noted, not filed: SelectField.tsx still carries '(config as any)?.name' although config is typed SelectFieldMetadata — a dead cast outside this card's key. 承接者:无", "noted, not filed: LookupField ignores a bare-string dependsOn that the option widgets honour (core resolveDependsOnFields wraps a string); unreachable through the declared array-only type and consistent with the spec; reachable only from untyped hosts. Bearer: #7357 edits exactly that block when it drops the snake_case arm", "noted, not filed: LookupField's two-arm precedence is unchanged (depends_on first); moot once #7357 lands" ], "gates": { "gates": { "pnpm --filter @object-ui/types type-check": "exit 0 (3 legs; VERDICT command-exit 0)", "pnpm --filter @object-ui/fields type-check": "exit 0 after closure build (first run exit 2 = 113 x TS2307 NOT MEASURED)", "turbo run build --filter=@object-ui/fields^... --concurrency=2": "exit 0 (9/9 tasks)", "vitest run (20 files)": "exit 0 — Test Files 20 passed (20), Tests 252 passed (252)", "eslint (11 changed files, package spelling)": "exit 0 — 0 errors, 73 pre-existing warnings, 0 attributable", "check-control-bytes": "exit 0 — OK (scanned 6796 tracked text files)", "check-changeset-presence": "exit 0 — 11 source file(s) of 2 released package(s) changed, declares 2 changeset(s)", "check-changeset-no-major": "exit 0", "check-changeset-fixed": "exit 0", "check-changeset-overwrite": "exit 0 — 2 added, 0 modified, 0 deleted", "check-doc-fence-languages": "exit 0", "check-spec-symbol-derivation": "exit 0 — nothing cites a key its spec symbol does not declare", "check-unreferenced-sources": "exit 0", "check-type-check-coverage": "exit 0 — 42/42 packages compile their tests", "check-doc-links": "exit 0 — Links are valid across 17 scan roots", "check-doc-component-types": "exit 0", "check-doc-example-shared-reader": "exit 0", "check-doc-expression-carriage": "exit 0 (report-only)", "check-governed-queue-guard --test (15 paths)": "exit 0 — NOT GOVERNED", "check-doc-snippet-types": "declared to CI (whole-workspace build); local proxy exit 0 with negative control exit 2 (TS2322)", "check-clause2-carriers --pair 8609": "exit 3 — PREREQUISITE NOT MET (REST 403 in this session), NOT MEASURED", "pnpm lint (full farm), check-designer-field-key-parity, check-handler-key-reads, check-i18n-*": "CI-owned, not run locally" }, "line_budget": "not applicable — no skills/** file in the diff", "deviations": [ "member typed FieldDependsOn (spec-derived, array-only) instead of the literal DependsOnInput the ruling's parenthesis names — see open_questions[0]", "fields type-check first run NOT MEASURED (fresh worktree, no sibling dist) — measured green after building the @object-ui/fields^... closure", "eslint run with the package spelling (no --no-inline-config; that flag is objectstack's)", "check-doc-snippet-types declared to CI; local two-snippet proxy against built dist with a negative control", "check-clause2-carriers --pair 8609 NOT MEASURED (exit 3, REST 403)", "branch merged origin/main once (one commit) before the final push; full local union re-run on the merged head 7ac7367" ], "files_changed": [ ".changeset/fields-depends-on-declared-read-6153.md", ".changeset/types-field-depends-on-declared-6153.md", "content/docs/fields/lookup.mdx", "content/docs/fields/select.mdx", "packages/fields/src/widgets/CheckboxesField.tsx", "packages/fields/src/widgets/LookupField.dependsOnDeclared-6153.test.tsx", "packages/fields/src/widgets/LookupField.tsx", "packages/fields/src/widgets/MultiSelectField.tsx", "packages/fields/src/widgets/RadioField.tsx", "packages/fields/src/widgets/SelectField.dependsOnDeclared-6153.test.tsx", "packages/fields/src/widgets/SelectField.tsx", "packages/fields/src/widgets/toHostProps.ts", "packages/types/src/__tests__/field-metadata-depends-on-declared-6153.test.ts", "packages/types/src/field-types.ts", "packages/types/src/index.ts" ] } }Generated by Claude Code
os-warren commentedon Sep 8, 2026
Contract review verdict — PASS, adopted VERBATIM. Both carriers CLEARED
domain:spec@ objectui seat, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w, reading taken 2026-09-08T16:39Z.Tier verification, before adopting a word:
⇒ Ran at
CONTRACT_REVIEW_TIERthroughout. Adopted verbatim, ⛔ not rewritten or trimmed.Contract review — objectui PR #8609 (issue #6153, instance 1)
Reviewed at PR head
7ac7367(merge ofcbbca66ontoorigin/main19ca447;git show 7ac7367 --statis empty, i.e. a clean merge with no conflict hunks;git diff origin/main...pris exactly the 15 declared files). All measurements below were made in a fresh worktree at that head afterpnpm install --frozen-lockfile; installed@objectstack/specis 17.3.0 in bothpackages/typesandpackages/fields(lock resolution@objectstack/spec@17.3.0).① Derived judgments
⭐ The central question — sentence vs parenthesis
Probe, re-derived, same call shape on every leg (
FieldSchema.safeParsefrom@objectstack/spec/data17.3.0), select document{name, type:'select', label, options:[…]}:dependsOn: ['country', {field:'region', param:'region_id'}]dependsOnBogusKeyReview: trueunrecognized_keysnaming it (the door is strict)dependsOn: 'country'(bare string)invalid_typeatdependsOn, expectedarraydependsOn: nullinvalid_typeatdependsOn, expectedarraydepends_on: ['country']unrecognized_keysnamingdepends_ondependsOn: [{field:'country'}](no param)dependsOn: [{field, param, bogus}]invalid_unionatdependsOn.0(entry object is$strict)dependsOn: [][{field:'account', param:'account_id'}]'account'invalid_typeatdependsOnThe spec's inferred
Field['dependsOn'](distobject.zod-*.d.ts) is(string | { field: string; param?: string | undefined })[] | undefined.DependsOnInput(form.ts:1008) isstring | Array<…> | undefined | null.Factual claim: CORRECT, and slightly understated — the spec refuses not only the bare string but also the
nullthatDependsOnInputadmits. Further, I searched the whole specdist(*.d.ts,*.d.mts) for anydependsOnadmitting a string at any level: none exists. Even the spec'suiFieldWidgetPropsSchema.dependsOn(the counterpart of objectui's widget prop) is array-only. SoDependsOnInputis not "the spec's shape" at any level — itsstring/nullarms are objectui's own form-face authoring tolerance (its doc comment cites framework#4074: "FormField.dependsOnsaidstring"), and theform.ts:1032comment "Aligns with @objectstack/spec FormField.dependsOn" is unverifiable at the type level because the spec'suiFormField"erases toany" (the repo's ownno-restricted-importsmessage atindex.ts:1298).Ruling on A vs B: A (shipped,
FieldDependsOn) is correct; B (DependsOnInput) would be wrong. Under B an author writesdependsOn: 'country'on select metadata, TypeScript accepts it, the four option widgets honour it at runtime (core'sresolveDependsOnFieldswraps a string),LookupFieldsilently ignores it (its guard isArray.isArray), and the publish door refuses it — declared ≠ enforced, in the hiding direction, which is the exact defect class this card and #6140 exist to close, and it would violate the ruling's own facet ③ (「写错拼写由类型系统响亮拒绝」). A is strictly narrower than B at the authoring face, assignable toDependsOnInputat every reader (pinned; and the fields type-check proves it at all six read sites), and no previously-typed code narrows because no declared member existed before.Was following the sentence over the parenthesis legitimate? Yes — and it was not a silent re-ruling. Three reasons, in order of weight:
BaseFieldMetadata声明dependsOn,五个 widget de-cast 到它" — mirror the protocol. No type name appears there. TheDependsOnInputparenthesis is the director seat's transcription, and it is measurably false. Following the parenthesis would have executed something the maintainer did not say over something the maintainer did say.dependsOnand de-cast the five widgets; every pin the ruling names (gate, scope, showcase paths, annotatedSelectFieldMetadataliteral) is satisfied identically by A and B. The one thing that differs is whether the type admits shapes the spec refuses — and the ruling's stated purpose answers that.DependsOnInputagainst the installed spec with the two-control probe and said falsifying it is a good outcome; the dev did, and reported it asdeviations[0],open_questions[0], in the PR body, with a one-line reversal path — routed to this review, which is the compensating control for accept-set changes. That is the correct escalation path for a sub-decision of this size; re-summoning the maintainer for it would have been ceremony over substance.Recommendation to the PM seat (non-blocking): note on the card, in one line, that ruling 5510108593's parenthesis is superseded by measurement — so the next reader does not re-open this.
The rest of ①
FieldDependsOnderived by reference, not restated:export type FieldDependsOn = NonNullable<SpecField['dependsOn']>withimport type { Field as SpecField } from '@objectstack/spec/data'— the same modulefield-types.tsalready imported (ServiceObject,ObjectIndex) onmain. The pin_SpecShapeIsArrayOfNameOrEntry(invariantEqual) compiles, so a spec release that moves the shape turns it red.packages/types/src/index.ts:470; builtdist/index.d.ts:76carriesFieldDependsOn;dist/field-types.d.ts:37,160.grep -rE "as any\)\??\.dependsOn|config\??\.dependsOn"overpackages/fields/src(non-test) → none.fieldis typedFieldMetadata(widgets/types.ts:136). Ablation leg 1 re-derived: deletingdependsOn?: FieldDependsOn;→ types test project exit 2 (8 errors: 5×TS2339, 3×TS2561, all in the new pin); after rebuildingtypes/distwithout the member,packages/fieldstsc --noEmitexit 2 at exactly six sites —CheckboxesField.tsx(44),LookupField.tsx(309),LookupField.tsx(317),MultiSelectField.tsx(45),RadioField.tsx(43),SelectField.tsx(101). Restore byte-verified (blob hash equals HEAD, dist count back to 1, both checks green). Leg 2: reintroducing(config as any)?.dependsOninSelectField→ read-site pin 2 failed / 9 passed; restore → 11/11.LookupFieldarms present, precedence unchanged:LookupField.tsx:309cascadeMeta?.depends_on ?? cascadeMeta?.dependsOn; the guard rewrite (if (!raw || !Array.isArray(raw)) return []) is logically identical to the old two-step. OnlyLookupFieldreadsdepends_oninside@object-ui/fields.field-types.tsdiff has noSelectOptionMetadata/descriptionline;SelectOptionMetadata.description?: stringfrom feat(types,fields): declare rows (markdown/html) and select-option description; de-cast the widget reads (objectui half of the #6140/#6153 family) #7061 stands.zod-mirror-parity.test.tsuntouched (git diff --quiettrue);src/zod/has no field-types mirror (its only mention is the ledger header disclaimer at line 39); the ledger's runtime passed in my vitest run.plugin-grid/relationalMetaKeys.ts:367already carriesdependsOn: { verdict: 'spec' }— not moved.content/docs/releases/untouched.FormField(dependsOn: DependsOnInput) into aFieldMetadatawould have compiled before and could fail now. Measured:turbo run buildover the doc-snippet gate's 34-package closure — exit 0 (every package'stsc); thenturbo run type-checkacross the whole workspace — exit 0, 81/81 tasks (everytsc --noEmitand everytsconfig.test.json). The candidate sites (plugin-form/sectionFields.ts:226,plugin-detail/fieldEnrichment.ts, app-shellObjectFieldInspector.tsx:1239) areas any/Record<string, any>/Record<string, unknown>, so untyped by construction. No consumer breaks.select.mdx:49does show the form schema with"dependsOn": "country"that the new paragraph refers to; the two newtssnippets pass the real gate (below). One precision nit, non-blocking:select.mdxsaysdepends_onis "read only by the lookup widget" — true for@object-ui/fields, butapp-shellreads it at three sites (ObjectFieldInspector.tsx:1239,resolveActionParams.ts:551,paramToField.ts:185) anddata-protocol.ts:737has an unrelatedAdvancedValidationRule.depends_on. That census belongs to RetireBaseFieldMetadata.depends_on— objectui's snake_case twin of the spec's field-leveldependsOn(enforce-or-remove; objectui#6153 ruling A follow-up) #7357's retirement, which should not take the doc sentence as its population.check-changeset-presence(11 source files of 2 released packages, 2 changesets),-no-major,-fixed,-overwrite,check-spec-symbol-derivation(docs cite no key the spec symbol lacks),check-doc-fence-languages,check-control-bytes,check-designer-field-key-parity,check-handler-key-read-sites,check-doc-component-types,check-doc-example-shared-reader,check-unreferenced-sources,check-doc-links,check-type-check-coverage(42/42),check-readme-exports(exit 1 on an unbuilt tree — identical onorigin/main, "population COLLAPSED"; exit 0 once dists were built). vitest on the 3 new pins +SelectField.cascade,LookupField.dependsOn,undeclared-but-consumed-keys-6150,zod-mirror-parity: 7 files, 139 tests, all pass.② Semver grading
@object-ui/types: minor — correct. Rule: an additive change to a published type surface (a new optional member onBaseFieldMetadata, inherited by everyFieldMetadatamember, plus a new exported type) is a feature-level widening →minor;patchis reserved for changes with no public-surface delta. Lane precedent agrees: feat(types): declare RichtextFieldMetadata for the widget's third registry key #8373 (RichtextFieldMetadatadeclared) and PR fix(types): declare the six spec members on record:details sections (objectui#8583, item 1) #8601's head both grade a declared-member additiontypes: minor. (AGENTS.md forbidsmajorhere; not relevant to this PR.)@object-ui/fields: patch — correct. Rule: no exported signature or type changes (FieldWidgetComponentProps.dependsOnunchanged; the only import added isimport type), and runtime behaviour is identical (configandfieldare the same object; theLookupFieldguard is equivalent) → internal read-path refactor →patch. The sibling failure was a published-type widening gradedpatch;fieldshas no type widening, so that failure does not transfer. Note for the record:.changeset/config.jsonplaces both packages in onefixedgroup, so the released bump isminorfor the group regardless; the per-package grades are the accurate record of each change's nature.③ Boundary flags
fieldstype-check first run exit 2, 113×TS2307, fresh worktree: acceptable. TS2307 "Cannot find module '@object-ui/…'" on an unbuilt tree is an environment reading (workspaceexportsresolve todist; CI'stype-checkjob waits on^build), correctly reported NOT MEASURED. I measured it green after the closure build, and again in the 81/81 workspace run. Not blocking.check-doc-snippet-typesdeclared to CI: now measured, not deferred. I built its--build-filterclosure (34 packages) and ran the real gate: exit 0, "633 of 633 block(s) judged, 0 failed";select.mdxandlookup.mdxare not inUNGATED_DOCS, so the two new snippets are inside that 633.doc-snippet-types.ymlalso triggers onpull_requesttomain. Not blocking.check-clause2-carriers --pair 8609exit 3: reproduced from this session (same token-refused 403;check-half-states --probeclassifies it as "the transport authenticates but repo-scoped reads are refused" —GET /rate_limit200,GET /repos/…403 with no rate-limit headers, i.e. something between node and GitHub). Then judged through the gate's own offline path: a--pair-jsondocument assembled from the PR row, card labels, and card comments I read via the GitHub API (the gate's design: a document can only make it say less) → exit 0, "the clause-② declaration is readable in the fixed spelling and both carriers agree." The limbs are also readable by hand: path limb (diff touchespackages/types/src/), declaration limb (claim 5587745845Clause-②: yes), label on both carriers (issue and PR both carryneeds:contract-review). Not blocking; the PM seat may re-run--pair 8609from a REST-open seat for the record.VERDICT:(case-sensitive) and it readsImplemented-by:/Reviewed-by:as a branch token vs asession_…token — the four lines below are in that grammar except the marker's case, which the task's format fixes asVerdict:.lintisturbo run lint→ per-packageeslint .; no workflow or script in objectui passes--no-inline-config. Re-measured: package spelling exit 0, 0 errors, 73 warnings (none on changed lines);--no-inline-configexit 1 with the single pre-existingno-restricted-importserror atindex.ts:1298, outside the diff (the PR's onlyindex.tsline is 470). Not blocking.origin/mainbefore the final push: the dispatch itself instructed it; the merge commit is clean (no conflict hunks) and brought in only docs(types): record the #6150 ledger's blind spot — complete only for the un-cast spelling #8602's test and its changeset. Not blocking.Out of scope, noted for the bearers
FieldWidgetComponentProps.dependsOn?: DependsOnInput(widget prop) is wider than the spec's ownFieldWidgetPropsSchema.dependsOn(array-only) — A component type has THREE declared surfaces that disagree (TS schema type / registry meta inputs / renderer prop reads), and nothing reconciles or enforces them #4631's face reconciliation, not this card.RadioField/CheckboxesFieldkeepconst config = field as anyforoptions/name;SelectFieldkeeps(config as any)?.name— outside this key, as the PR body says.Seat action
Carrier CLEARED on both載體 —
needs:contract-reviewstripped from this card and PR #8609 in one stroke. Clearing rule satisfied: the verdict's own measurements cover the whole change, and itsNOT MEASUREDitems (a live network run of a gate it judged offline instead, the live e2e lane, CI-owned farm scripts) disclaim no part of the diff.⭐ Recorded as the reviewer asks, so nobody re-opens it
Ruling
5510108593's parenthesisDependsOnInputis SUPERSEDED by measurement. The maintainer's 「同意」 attached to a framing that names no type at all — "objectui 元数据面镜像协议:BaseFieldMetadata声明dependsOn,五个 widget de-cast 到它". The type name was the director seat's transcription and is measurably false: nodependsOnanywhere in the spec's dist admits a string, at any level. The shippedFieldDependsOnexecutes what the maintainer said; the parenthesis would have executed what the maintainer did not.This seat judged the gate's offline
--pair-jsonpath impractical and recordedcheck-clause2-carriersas UNREAD. The reviewer assembled that document from API reads and ran it — exit 0, both carriers agreeing. ⇒ My call was too conservative: the path is usable from this seat class, and the seat post's platform note is being corrected accordingly. ⛔ The 403 on the network path is still real; what was wrong was concluding the gate could not be answered at all.Next: landing pre-checks on a fresh reading, then ready → enqueue.
Generated by Claude Code
os-warren commentedon Sep 8, 2026
⛔ CI RED on PR #8609 — patch round opened. Enqueue held; the cleared carrier will be RE-HUNG on the new head
domain:spec@ objectui seat, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w, reading taken 2026-09-08T16:42Z (origin/main=0544ae9).The contract review PASSed. This is a separate, independent failure, and it is genuinely this PR's — ⛔ not a flake, and ⛔ not a re-run candidate.
The failure, root-caused rather than labelled
That pin scans
packages/fields/src/widgets/LookupField.tsxby text for the reads its copy-set table declares. This PR renamed the local carrier (fieldMeta→cascadeMeta) and routed the read through the annotatedLookupFieldMetadataalias — so the scanner stops matching, whileLookupFieldstill genuinely reads both keys.⇒ The reads are real; the scanner no longer sees them.
Test Files 1 failed | 687 passed,Tests 1 failed | 9045 passed— one assertion, and it is a true statement about a text pattern rather than about behaviour.⭐ Why this got through every local gate, which is the part worth keeping
The coupling is cross-package and by text scan: a test in
plugin-gridreads a source file infields. It is therefore invisible to a file-surface declaration — the discipline this lane uses to prove independence. The dev's declared surface waspackages/types+packages/fields+ docs, which was accurate; the dev's local vitest run covered 20 relevant files; the contract reviewer ran the whole-workspaceturbo run type-check(81/81) and checkedrelationalMetaKeys.ts— but a runtime text-scan pin is not a type error, so none of those three could see it.The patch round, and the fence on it
Sent to the original dev. The fence matters more than the fix:
⛔ Deleting the reader entry from the copy-set table, or weakening the pin, is forbidden. Removing the entry would assert that
LookupFieldstopped reading these keys — which is false — and would handdependsOna wrong verdict in a table other code trusts. The pin's own words are that "a stale reader entry is how a key keeps a copy licence it has stopped earning"; making it green by lying to it inverts its purpose.✅ Two legitimate fixes: teach the scanner the new spelling, or spell the read so the existing scanner sees it. The dev reads that test's derivation logic, picks, and says which and why. It is also asked to check whether other consumers scan
packages/fields/src/widgets/**the same way — one shard at a time is a bad way to find out.Carrier consequence — stated now so it is not read as backsliding
needs:contract-reviewwas cleared on this card and PR #8609 on the verified PASS at head7ac7367. Pushing the fix moves the head, and the rule is 「head 后移或无结论才重挂」 ⇒ the carrier gets re-hung on both and the increment re-reviewed atCONTRACT_REVIEW_TIER.⛔ That is the protocol working, ⛔ not a reversal of the PASS: that verdict remains valid for what it measured, and it will not be reconstructed from memory — the un-covered increment gets a fresh review, as 「⛔ 永不从记忆重构缺失的裁决」 requires.
⛔ PR #8609 stays draft, ⛔ not enqueued, ⛔ no auto-merge.
Generated by Claude Code
os-warren commentedon Sep 8, 2026
os-dev-report
Patch round (supersedes the report at comment 5588301571 for head, gates and files; everything else there stands).
{ "issue": 6153, "status": "done", "branch": "claude/issue-6153-dependson-declared-member", "pr": "https://github.com/objectstack-ai/objectui/pull/8609", "premise_still_valid": true, "summary": "Patch round for the CI red on packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.ts (Test shard 1/4, deterministic). That pin re-extracts LookupField's reads by scanning the source for member accesses off the receiver identifier fieldMeta; my change reads dependsOn / depends_on through an annotated alias (const cascadeMeta: LookupFieldMetadata | undefined = fieldMeta;), so the scanner stopped seeing two reads LookupField still performs and the orphan assertion called them stale. FIX TAKEN: the scanner, not the read-site spelling — memberReads now sweeps the receiver plus every annotated alias of it (const NAME(: TYPE)? = receiver;), derived from the consumer's own source on every run with no alias name listed in the test; the copy-set table (RELATIONAL_META_READ_SET) is untouched and both reader entries stay. WHY: spelling the read back onto the untyped fieldMeta would put the read through any again — the de-casting the card is about — and the type-level ablation would no longer turn LookupField red; the extractor's header already states its reach as member reads off named receivers, and an annotated alias is the same object read through a declared type, so widening the reach there (with controls) is the correct half. Controls added: the two keys must be found through the alias (positive control, so an extractor that stops following aliases fails loudly before the orphan check does); an inline-fixture control pinning what the alias rule reaches (direct read, annotated alias, bare alias, a member read off the receiver) and what it must not (a different receiver; a name initialised from a member read); and the real consumers' alias census (LookupField ['fieldMeta','cascadeMeta'], UserField ['meta']). SIBLING SWEEP: no other test or gate scans packages/fields/src/widgets by the renamed identifiers — the three referenceArms-6837 tests (plugin-detail, plugin-gantt, plugin-tree) name LookupField.tsx only in prose and deliberately do not scan it; lookupPickerKeys-7154 and relationalMetaKeys.ts do not read it; the generic scanners (check-package-self-import, check-vi-mock-inherit, check-doc-component-types, check-i18n-call-site-keys, i18n common-search-retired-4392) match widgets/ only as a path and key on nothing my diff renamed (the two 'dependsOn' hits in check-package-self-import.mjs are turbo's dependsOn in comments). New head 9b1df34 (one commit on top of 7ac7367, one file). Worktree re-created for the round and removed after the push.", "tests": "HEAD 9b1df34, under scripts/pm/os-verify-lock.sh, exits captured before any pipe. vitest run --maxWorkers=2 on 14 files (relationalMetaCopySet.derivation, lookupPickerKeys-7154, field-metadata-depends-on-declared-6153, SelectField.dependsOnDeclared-6153, LookupField.dependsOnDeclared-6153, SelectField/MultiSelectField/RadioField/CheckboxesField .cascade, optionWidgets.gatedOptions, LookupField.dependsOn/.gateHintLabel/.optionDescription, field-metadata-rows-option-description-6140): exit 0 — 'Test Files 14 passed (14)', 'Tests 94 passed (94)'. pnpm --filter @object-ui/plugin-grid type-check (tsc --noEmit && tsc -p tsconfig.test.json): first run exit 2 = 51 x TS2307 'Cannot find module @object-ui/...' plus its implicit-any fallout (45 x TS7006, 1 x TS2339, 1 x TS7031) on a fresh worktree with no sibling dist = NOT MEASURED; after turbo run build --filter='@object-ui/plugin-grid^...' --concurrency=2 (12/12 tasks, all cache hits) the && chain build-then-type-check gave VERDICT command-exit 0. eslint packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.ts: exit 0, 0 errors, 1 pre-existing warning (the (FieldSchema as any).shape line, now 249). check-control-bytes exit 0 (6802 files); check-changeset-presence exit 0 ('12 source file(s) of 3 released package(s) changed, and this change declares 2 changeset(s)' — the plugin-grid change is test-only, covered by the existing declaration); check-changeset-overwrite exit 0; check-changeset-no-major exit 0; check-changeset-fixed exit 0. ABLATION of the fix from commit 9b1df34, trap-restored, restore = git checkout HEAD -- PATH, blob compared to HEAD's: reverting the alias sweep to the pre-fix reach (receiver only; mutation on disk count 1, alias-sweep line 0) => derivation test exit 1, 'Tests 4 failed | 10 passed (14)' — the orphan assertion reproduces CI's exact text (expected [ 'dependsOn', 'depends_on' ] to deeply equal []), plus the two CONTROLs and the #7155 dialect assertion; restore: git diff HEAD empty, blob equals HEAD, 'Tests 14 passed (14)'. Direction observed: red, as expected. The earlier full union on 7ac7367 (types/fields type-checks, 20-file vitest, 13 cheap gates, doc-snippet proxy with negative control) stands — this round changed one test file in plugin-grid and nothing under packages/types, packages/fields or content/docs.", "mcp_calls": "12 — 9 from the first round, plus add_issue_comment x2 (this report, PR note) and issue_read get_comments (report read-back)", "open_questions": [ { "question": "Unchanged from the first report: the member is typed with the spec-derived FieldDependsOn (array-only) rather than the literal DependsOnInput the ruling's parenthesis names; contract review PASSed on 7ac7367 and is re-hung on 9b1df34 by protocol.", "options": ["A keep FieldDependsOn (ships)", "B retype as the literal DependsOnInput"], "recommendation": "A, for the reasons in comment 5588301571" }, { "question": "Still NOT MEASURED from this seat: check-clause2-carriers --pair 8609 (exit 3, REST 403). needs:contract-review remains on the PR; the body carries 'Clause-②: yes'.", "options": ["A the PM seat runs --pair 8609 from a REST-open seat or hands it --pair-json", "B accept the carriers read back"], "recommendation": "A" } ], "out_of_scope_findings": [ "noted, not filed (unchanged): RadioField / CheckboxesField keep field as any for options / name — no radio / checkboxes member in the FieldMetadata union. Bearer: #4631; otherwise 承接者:无", "noted, not filed (unchanged): SelectField.tsx (config as any)?.name is a dead cast. 承接者:无", "noted, not filed (unchanged): LookupField ignores a bare-string dependsOn the option widgets honour; unreachable through the declared type. Bearer: #7357", "noted, not filed (new, this round): the copy-set extractor's alias rule is the only place in the repo that knows a consumer may read its bag through a typed alias; #4631's de-casting of LookupField's camelCase-fallback family will add aliases the census control in this test pins (['fieldMeta','cascadeMeta']) and must update that expectation in the same stroke. Bearer: #4631" ], "gates": { "gates": { "vitest run (14 files incl. relationalMetaCopySet.derivation)": "exit 0 — Test Files 14 passed (14), Tests 94 passed (94)", "pnpm --filter @object-ui/plugin-grid type-check": "exit 0 after closure build (first run exit 2 = 51 x TS2307 NOT MEASURED)", "turbo run build --filter=@object-ui/plugin-grid^... --concurrency=2": "exit 0 (12/12, cached)", "eslint (1 changed file)": "exit 0 — 0 errors, 1 pre-existing warning", "check-control-bytes": "exit 0 — OK (6802 tracked text files)", "check-changeset-presence": "exit 0 — 12 source file(s) of 3 released package(s) changed, declares 2 changeset(s)", "check-changeset-overwrite": "exit 0", "check-changeset-no-major": "exit 0", "check-changeset-fixed": "exit 0", "first-round union on 7ac7367 (types/fields type-check, 20-file vitest, 13 cheap gates, doc-snippet proxy)": "stands — this round touched none of those paths", "check-clause2-carriers --pair 8609": "exit 3 — PREREQUISITE NOT MET (REST 403), NOT MEASURED", "pnpm lint (full farm) and the remaining check:* scripts": "CI-owned, not run locally" }, "line_budget": "not applicable — no skills/** file in the diff", "deviations": [ "fix taken in the scanner (packages/plugin-grid test), not in LookupField's spelling — the alternative would re-cast the read the card removed", "plugin-grid type-check first run NOT MEASURED (fresh worktree, no dist) — green after the closure build", "check-clause2-carriers --pair 8609 NOT MEASURED (exit 3, REST 403)" ], "files_changed": [ "packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.ts", "(first round, unchanged) .changeset/fields-depends-on-declared-read-6153.md", "(first round, unchanged) .changeset/types-field-depends-on-declared-6153.md", "(first round, unchanged) content/docs/fields/lookup.mdx", "(first round, unchanged) content/docs/fields/select.mdx", "(first round, unchanged) packages/fields/src/widgets/CheckboxesField.tsx", "(first round, unchanged) packages/fields/src/widgets/LookupField.dependsOnDeclared-6153.test.tsx", "(first round, unchanged) packages/fields/src/widgets/LookupField.tsx", "(first round, unchanged) packages/fields/src/widgets/MultiSelectField.tsx", "(first round, unchanged) packages/fields/src/widgets/RadioField.tsx", "(first round, unchanged) packages/fields/src/widgets/SelectField.dependsOnDeclared-6153.test.tsx", "(first round, unchanged) packages/fields/src/widgets/SelectField.tsx", "(first round, unchanged) packages/fields/src/widgets/toHostProps.ts", "(first round, unchanged) packages/types/src/__tests__/field-metadata-depends-on-declared-6153.test.ts", "(first round, unchanged) packages/types/src/field-types.ts", "(first round, unchanged) packages/types/src/index.ts" ] } }Generated by Claude Code
os-warren commentedon Sep 8, 2026
Re-review verdict — PASS, adopted VERBATIM. Both carriers CLEARED again; enqueue held on 3 test shards
domain:spec@ objectui seat, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w, reading taken 2026-09-08T17:00Z.Tier verification: transcript 343,371 bytes · 58 harness-stamped
modeloccurrences · distinct values:claude-fable-5-1only · non-fable stamps 0 · control fired. ⇒ Ran atCONTRACT_REVIEW_TIER. Adopted verbatim.Re-review — objectui PR #8609, increment
7ac7367..9b1df34(issue #6153)Measured in a fresh detached worktree at
9b1df34(pnpm install --frozen-lockfile --prefer-offline, warm store), vitest launched from the repo root per the objectui#3378 guard. Primary checkout untouched; worktree removed afterwards.① The increment — confirmed, nothing reviewed moved
git diff --stat 7ac7367 9b1df34packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.tsonly9b1df34's sole parent is7ac7367;merge-base=7ac7367packages/fields,packages/types,content/docs,.changesetRELATIONAL_META_READ_SETin the diffpackages/plugin-grid/src/relationalMetaKeys.tsgit diff --quiettrue vs both7ac7367and base19ca447The prior PASS at
7ac7367(comment 5588610450) transfers: no line it reviewed changed. I did not re-derive its axes ①②③.② Was the scanner the right half? Yes.
fieldMetaisanyby construction:LookupField.tsx:242const lookupField = field as any;→:254const fieldMeta = … ? innerField : lookupField. Re-spelling the read asfieldMeta?.dependsOnputs it back throughany, which is the exact cast the card removes, and it would un-redden the reviewed ablation leg (TS2551 atLookupField.tsx:309/317when the member is deleted) — that leg only fires because the read goes throughLookupFieldMetadata.packages/types/src/__tests__/field-metadata-depends-on-declared-6153.test.ts:191,196pins the read site verbatim asconst cascadeMeta: LookupFieldMetadata | undefined = fieldMeta;/const raw = cascadeMeta?.depends_on ?? cascadeMeta?.dependsOn;. So the rejected alternative would have moved reviewed code inpackages/fieldsand the reviewed pin inpackages/types, voiding the transfer of the prior PASS. A cast(fieldMeta as LookupFieldMetadata).dependsOnis likewise refused by that pin's "no reader reachesdependsOnthrough a cast" assertion. TypingfieldMetaitself is A component type has THREE declared surfaces that disagree (TS schema type / registry meta inputs / renderer prop reads), and nothing reconciles or enforces them #4631's population by the file's own comment.③ The adversarial question — reach of
const NAME(: TYPE)? = receiver;I copied
receiverAliases/memberReadsverbatim into a probe (scratchpad/probe.mjs, 31 alias inputs + 14 read inputs) and ran ablations in the tree.What the rule reaches: whole-object
constaliases of the exact receiver token — bare, annotated, union-annotated,export const, generic annotation, multi-line annotation. What it does not reach (all false negatives, i.e. loud, not quiet):let/var,as-cast,!,?? {},wrap(receiver),(receiver), ternary,===, destructuring, second declarator, missing;(ASI), annotation containing;(object-literal type) or=>(function type), alias-of-alias,receiverX/myreceiver,props.receiver, and — as the fixture pins —const x = receiver.key;.Could I construct a case the new rule wrongly accepts? Yes — three, and none is new in kind:
7ac7367) via the receiver name// cascadeMeta.dependsOn …)// fieldMeta.dependsOn …)fieldMeta)other.cascadeMeta.x— a property named like the aliasother.fieldMeta.x)Tree-level proof: E3a (new scanner, real
dependsOnread deleted, a comment namingcascadeMeta.dependsOnleft) → 14/14 pass, exit 0 — quietly green. E3b (old scanner, reads re-spelled ontofieldMeta,dependsOnread deleted, comment namingfieldMeta.dependsOn) → 13/13 pass, exit 0 — the identical quiet green before this PR. The textual extractor never distinguished code from comments or scopes; the alias rule extends that pre-existing trust to each alias name, and the alias-name set is pinned by the census (['fieldMeta','cascadeMeta']forLookupField.tsx,['meta']forUserField.tsx), so any new alias is a loud failure. Today no collision exists:cascadeMetaoccurs only atLookupField.tsx:307/309/317,fieldMetanever as.fieldMeta,metainUserField.tsxonly at 41–53.Does the pin still catch a real orphan through the alias? E2 (new scanner,
?? cascadeMeta?.dependsOnand its dep genuinely removed, 0 live reads) → 3 failed / 11 passed: the positive control (expected […] to include 'dependsOn'), the orphan assertion (expected [ 'dependsOn' ] to deeply equal []), and the #7155 dialect test. Red, loudly. E1 (old scanner against this tree) reproduces CI's red exactly: orphans['dependsOn','depends_on'].Do the controls constrain the reach, or rubber-stamp? Partly constrain. The fixture would fail a rule that drops the
;anchor (notAnAliaswould become an alias) or ignores receiver identity (other), and it pins both alias forms — those are real discriminations. It would not fail a looser future rule that also acceptedlet x = receiver;,const x = receiver as T;orconst x = wrap(receiver);— the first and third would be wrong (reassignable / not the same object). The census is the backstop there: such drift becomes loud only if the real consumer contains one of those forms, which it does not today. Non-blocking hardening: add negative fixture lineslet notConst = fieldMeta;andconst wrapped = wrap(fieldMeta);so the rule's exclusions are pinned as text, not only implied. Nit: the file-header docblock says "every ANNOTATED ALIAS" while the regex (and the function doc, and the fixture) admit a bareconst NAME = receiver;— the header is narrower than the rule; the reach is documented one level down.④ The fence holds
relationalMetaKeys.ts:367dependsOn: { verdict: 'spec', readers: LOOKUP_EDITOR_ONLY, … }and:372depends_on: { verdict: 'no-producer', readers: LOOKUP_EDITOR_ONLY, … }both survive; the file is byte-identical to base.receiverAliases,memberReads, two addedtoContainlines in the positive control, and one new CONTROL test). The pin is strengthened, not weakened: the positive control now also requiresdependsOn/depends_onto be extracted, so an extractor that stops following aliases fails there before the orphan check.9b1df34: 14/14 pass, exit 0. Every ablation restored byte-identical to HEAD (git diff --quietverified each time).CI at reading time (16:56Z): Type Check, Lint, Doc Snippet Type Check, Build & E2E, all changeset gates — success; Test shards 1–4 still
in_progress(not waited on; the affected file measured locally instead).Seat action
Both carriers CLEARED —
needs:contract-reviewstripped from this card and PR #8609 in one stroke. Clearing rule satisfied: the verdict's measurements cover the increment, and its carry-forward of the prior PASS rests on a byte-level proof that no reviewed file moved, ⛔ not on an assumption.⭐ The answer to the adversarial question is the valuable part
I asked whether widening a scanner's reach could make it stop catching the very orphans it guards, and required a constructed counter-example. It found three — and then did the thing that turns a worry into a reading: it proved with E3a/E3b that the identical quiet-green exists under the old scanner too. ⇒ The widening extends a pre-existing trust (the extractor never distinguished code from comments or scopes); it ⛔ does not introduce a new blind class. And E2 shows the pin still goes loudly red — three assertions, including the positive control — when a read is genuinely removed.
⇒ The guard is strengthened, not weakened: the positive control now also requires the keys to be extracted, so an extractor that stops following aliases fails before the orphan check.
## 验收备注— two non-blocking items, ⛔ not filed as cardsBoth are inside a test file's own scaffolding, have a named bearer, and neither is a published-contract defect ⇒ neither meets the three filing classes.
let x = receiver;orconst x = wrap(receiver);(both wrong — reassignable / not the same object). Suggested negative fixture lines pin those exclusions as text rather than leaving them implied. 承接者: whoever next edits that extractor — plausibly A component type has THREE declared surfaces that disagree (TS schema type / registry meta inputs / renderer prop reads), and nothing reconciles or enforces them #4631, which the dev already flagged will add aliases the census control pins.const NAME = receiver;.⛔ Enqueue held: 30 of 33 checks green, zero failures, 3 test shards still
in_progressat 17:00Z. The bar is every check, ⛔ not the required subset — enqueue happens on a fresh reading once they land.Generated by Claude Code
os-warren commentedon Sep 8, 2026
Enqueued — PR #8609 flipped ready and armed, 2026-09-08T17:07Z
domain:spec@ objectui seat, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w.origin/main=9f5de79at this reading.Landing pre-checks, taken at the moment of enqueue
9b1df34(5588867430), transcript-verified: 58 stamps, single valueclaude-fable-5-1, 0 non-fable. Carrying forward the earlier PASS (5588610450) on a byte-level proof no reviewed file movedcheck-clause2-carriers --pair 8609— live path unavailable in this seat class (exit 3, REST 403, reproduced independently by the dev). ⛔ Recorded as read only because the review subagent ran the gate's own offline--pair-jsonpath and got exit 0 ("the clause-② declaration is readable in the fixed spelling and both carriers agree"), with the three limbs also read by handTest (shard 1/4)— the one that was RED on7ac7367— re-read individually by check-run id:conclusion: successGoverned Surface Queue Guardsuccess⇒
draft: false, auto-merge armed (squash). ⛔ Not merged by this seat.On merge
This card closes via
Fixes #6153. Aftercare owed at that point: strippm:dispatchedand clear the assignee in one stroke.⭐ And #7357 becomes unblocked. It is
pm:blockedon this card, and it is the card that removes the snake_casedepends_onarm and theBaseFieldMetadata.depends_onmember. This PR deliberately kept both arms so that removal stays #7357's to make — the pin that keeps the legacy literal compiling is labelled for deletion in that stroke.5588867430for whoever next edits the copy-set extractor (probably #4631, which will add aliases the census control pins): the fixture should pin the rule's exclusions as text (let x = receiver;,const x = wrap(receiver);), and the file-header docblock claims a narrower reach ("every ANNOTATED alias") than the rule actually has.Generated by Claude Code
os-warren commentedon Sep 8, 2026
MERGED — aftercare complete, and #7357 is unblocked
domain:spec@ objectui seat, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w, reading taken 2026-09-08T17:47Z.PR #8609 merged; this card closed
completedat 17:24Z by itsFixeskeyword.pm:dispatchedstripped and the assignee cleared in one label write —domain:speckept, because 归属不是状态.Verified by content on the tree, ⛔ not by an API field
list_pull_requestsreturnedmerged: falsealongside a populatedmerged_atearlier today, so neither field was trusted on any landing this shift.What landed
BaseFieldMetadata.dependsOn?: FieldDependsOn, whereFieldDependsOnis derived by reference from@objectstack/spec'sFieldrather than restated — so a spec release that moves the shape turns the pin red instead of drifting silently. Five reader widgets de-cast onto the declared type; bothLookupFieldarms deliberately kept for #7357; docs restored; two changesets (typesminor,fieldspatch).⭐ The ruling's parenthesis was superseded by measurement, and that is the part worth carrying: it named
DependsOnInput, which admits a bare string andnullthat the spec'sFieldSchemarefuses withinvalid_type. The maintainer's 「同意」 attached to a framing containing no type name at all — "objectui 元数据面镜像协议". Following the parenthesis would have shipped a type wider than the contract, which is the exact defect this card existed to close. Two independent ceiling-tier reviews confirmed that reading and confirmed it was not a silent re-ruling.Downstream
⭐ #7357 is UNBLOCKED and back in
pm:queue— itsBlocked-by:was this card. Its premises were re-verified on86ef0c7before it returned (the snake member, the read arm and both docs mentions are all still there), and it was warned that #6153 moved the read site's spelling (cascadeMeta?.depends_onat:309, notfieldMeta?."around 300") and that removing the arm will legitimately move theplugin-gridcopy-set pin.plugin-gridreadLookupField.tsxby identifier, and the alias rename made two live reads invisible to it. It was root-caused and fixed in the scanner, ⛔ never by deleting the reader entry.Generated by Claude Code