Repository navigation
finding(fields): two more undeclared-but-consumed metadata keys — dependsOn on select and options[].description on lookup, both reached past the declared type #6153
Description
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
packages/fields/src/widgets/SelectField.tsx:98:
const dependsOn = (config as any)?.dependsOn ?? dependsOnProp;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:
const staticOptions: LookupOption[] = fieldMeta?.options || [];and LookupField.tsx:526 reads the key:
(opt.description && opt.description.toLowerCase().includes(q))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
dependsOntoSelectFieldMetadata(or migrate the widget to the declareddepends_on), anddescriptiontoSelectOptionMetadata. 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/ adescription_fieldcover 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).
Generated by Claude Code
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