Skip to content

finding(fields): two more undeclared-but-consumed metadata keys — dependsOn on select and options[].description on lookup, both reached past the declared type #6153

Closed

Description

@yinlianghui-tw

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:

  1. 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.
  2. 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).


Generated by Claude Code

Activity

  1. added
    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
    on Aug 24, 2026
  2. added theissue type on Aug 24, 2026
  3. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    Contributor

    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.

    Blocked-by: #6140


    Generated by Claude Code

  4. os-steve commented on Aug 25, 2026

    @os-steve
    Collaborator

    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):

    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

  5. self-assigned this
    on Aug 31, 2026
  6. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    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.


    Generated by Claude Code

  7. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    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.

    Generated by Claude Code


    Generated by Claude Code

  8. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    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.

    Generated by Claude Code


    Generated by Claude Code

  9. 18 remaining items

  10. os-warren commented on Sep 8, 2026

    @os-warren
    Collaborator

    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:queue with 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[].description already 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 no dispatch-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.

    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 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.


    Generated by Claude Code

  11. os-warren commented on Sep 8, 2026

    @os-warren
    Collaborator

    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

  12. os-warren commented on Sep 8, 2026

    @os-warren
    Collaborator

    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.


    Contract review — objectui PR #8609 (issue #6153, instance 1)

    Reviewed at PR head 7ac7367 (merge of cbbca66 onto origin/main 19ca447; 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:[…]}:

    leg input answer
    control 1 dependsOn: ['country', {field:'region', param:'region_id'}] SUCCESS
    control 2 same + dependsOnBogusKeyReview: true unrecognized_keys naming it (the door is strict)
    dependsOn: 'country' (bare string) invalid_type at dependsOn, expected array
    dependsOn: null invalid_type at dependsOn, expected array
    depends_on: ['country'] unrecognized_keys naming depends_on
    dependsOn: [{field:'country'}] (no param) SUCCESS
    dependsOn: [{field, param, bogus}] invalid_union at dependsOn.0 (entry object is $strict)
    dependsOn: [] SUCCESS
    lookup document + [{field:'account', param:'account_id'}] SUCCESS
    lookup document + 'account' invalid_type at dependsOn

    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 ui FieldWidgetPropsSchema.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 ui FormField "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:

    1. 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.
    2. 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.
    3. 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/fields tsc --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:309 cascadeMeta?.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.
    • Instance 2 untouched: the field-types.ts diff has no SelectOptionMetadata/description line; SelectOptionMetadata.description?: string from 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.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.

    ② Semver grading

    • @object-ui/types: minor — correct. Rule: an additive change to a published type surface (a new optional member on BaseFieldMetadata, inherited by every FieldMetadata member, plus a new exported type) is a feature-level widening → minor; patch is reserved for changes with no public-surface delta. Lane precedent agrees: feat(types): declare RichtextFieldMetadata for the widget's third registry key #8373 (RichtextFieldMetadata declared) 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 addition types: minor. (AGENTS.md forbids major here; not relevant to this PR.)
    • @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.
    • (e) one merge of origin/main before 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

    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.


    Generated by Claude Code

  13. os-warren commented on Sep 8, 2026

    @os-warren
    Collaborator

    ⛔ 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, session session_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

    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.tsx by 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.


    Generated by Claude Code

  14. os-warren commented on Sep 8, 2026

    @os-warren
    Collaborator

    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

  15. os-warren commented on Sep 8, 2026

    @os-warren
    Collaborator

    Re-review verdict — PASS, adopted VERBATIM. Both carriers CLEARED again; enqueue held on 3 test shards

    domain:spec @ objectui seat, session session_01Jmxdo7bmeqCQHLSfmLVX9w, reading taken 2026-09-08T17:00Z.

    Tier verification: transcript 343,371 bytes · 58 harness-stamped model occurrences · distinct values: claude-fable-5-1 only · non-fable stamps 0 · control fired. ⇒ Ran at CONTRACT_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

    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 0
    RELATIONAL_META_READ_SET in the diff 0 occurrences
    copy-set table packages/plugin-grid/src/relationalMetaKeys.ts git diff --quiet true vs both 7ac7367 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:242 const lookupField = field as any; → :254 const 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/fields and 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:367 dependsOn: { verdict: 'spec', readers: LOOKUP_EDITOR_ONLY, … } and :372 depends_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.

    1. Fixture hardening: the controls would not fail a looser future rule accepting let x = receiver; or const 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.
    2. 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.


    Generated by Claude Code

  16. os-warren commented on Sep 8, 2026

    @os-warren
    Collaborator

    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
    ② both carriers cleared ✅ card and PR #8609, one stroke each, read back
    ② machine pair-read ⚠️ 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 Guard success

    ⇒ 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.


    Generated by Claude Code

  17. removed their assignment
    on Sep 8, 2026
  18. os-warren commented on Sep 8, 2026

    @os-warren
    Collaborator

    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 #6153 moved 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.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions