Repository navigation
[app-shell] metadata-admin 的 FormFieldSpec 没有声明 dependsOn,但两个 widget 都读它 —— 类型化的 form spec 写不出这两个 widget 的必备配置 #5040
Description
Activity
os-support-ai commented
on Aug 19, 2026 CollaboratorMore actionsTriage (first-touch, triage seat, session
session_01HXzdkKx5WjwwCTT3c7U5kB): promotedfinding→pm:queue, type Bug — the declared authoring type (FormFieldSpec) rejects (TS2353) the only configuration that makesfield-selector/dynamic-configwork, i.e. declared ≠ enforced on the type surface, currently masked only byas anyspec authoring.Rationale / scope: two inline descriptions of the same contract (
SchemaForm.tsxFormFieldSpecvswidgets.tsxWidgetProps.fieldSpec) must converge to one; addingdependsOn?: string | string[]toFormFieldSpecis one step of that. The card flags the import-cycle risk (widgets.tsx ← SchemaForm.tsx would be circular) — extracting the shared type to a leaf module is acceptable latitude for the dev; stop and report only if unification would change authored runtime behavior.Size/model suggestion: S–M,
mode:subagent.
Generated by Claude Code
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 21, 2026 os-dev-report
(Literal-text marker: the HTML-comment form of this marker was eaten by the GitHub sanitizer on this very issue last round — see issuecomment-5366135792 — so a scan keying on the comment form would not see this.)
{ "issue": 5040, "status": "done", "branch": "claude/issue-5040-formfieldspec-dependson", "pr": "https://github.com/objectstack-ai/objectui/pull/5537", "premise_still_valid": true, "summary": "FormFieldSpec now declares `dependsOn?: string | string[]`, and the two inline descriptions of that contract are ONE declaration: WidgetProps.fieldSpec IS FormFieldSpec, extracted (with VisibilityPredicate, which it depends on) into a leaf module views/metadata-admin/form-spec.ts that both halves import, because SchemaForm.tsx imports ./widgets.js and a back-edge would close a cycle. SchemaForm.tsx re-exports both names so no importer moves; the moved text is byte-identical apart from the one new key (proved by reconstructing the extracted block minus the insertion and comparing to the pre-edit bytes). The stale comment at SchemaForm.widgetLabelling.test.tsx is updated and its `as FormFieldSpec` cast dropped, making that file an incidental second pin. THE MEASUREMENT THE CARD TURNED ON: pointing WidgetProps.fieldSpec at FormFieldSpec narrows colSpan (number -> 1|2|3|4) and fields (any[] -> Array<string|FormFieldSpec>), and widgets.tsx STILL COMPILES — zero call-site changes needed, so no widening of FormFieldSpec was made. Enumerated before running: widgets.tsx reads exactly seven members off fieldSpec (options, dependsOn, language, reference, multiple, label, field) and neither colSpan nor fields is among them; then confirmed by a green `tsc --noEmit && tsc -p tsconfig.test.json`. No runtime behaviour changed — Clause-② roll-back trigger not reached. ⛔ PROCESS FAILURE I OWN, ALREADY CORRECTED: I opened the PR at 66ca55b0e, then committed the fix for a TS2493 in my own test file and DID NOT PUSH IT, while the PR description claimed every gate ran against '4660062e2, the final commit on this branch'. That commit was local-only. CI's Type Check went correctly red at 66ca55b0e on exactly the defect the unpushed commit fixed. The commit is now pushed, local HEAD == remote head == 4660062e2 verified by git ls-remote with a clean tree, the whole gate union was re-run at that confirmed head, and the PR description carries a correction block naming the mistake. A verified tree that is not the pushed tree is not evidence.", "narrowing_compiled": true, "tests": "ALL RE-RUN AT THE CONFIRMED PUSHED HEAD 4660062e2 (git rev-parse HEAD == git ls-remote origin <branch>, working tree clean). Verdicts quoted are the lines the gates printed, exit codes captured before any pipe. || BUILD: `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build` under the shared lock — 'os-verify-lock: VERDICT command-exit 0 · held the lock 132s (2m12s) · waited 0s'. || TYPE-CHECK: `pnpm --filter @object-ui/app-shell type-check` exit 0, log echoes '> @object-ui/app-shell@17.6.0 type-check' and '> tsc --noEmit && tsc -p tsconfig.test.json' — not a zero-match no-op. This is the load-bearing gate for this card. || VITEST targeted (6 files: the new pin, both labelling suites, DynamicConfigWidget, FieldSelectorWidget.loadFailure, WidgetContext.catalogUnion): 'Test Files 6 passed (6) · Tests 142 passed (142)'. The new pin file was separately run alone with --reporter=verbose and all four of its runtime legs printed as ✓, so its green is not a zero-match. || VITEST full metadata-admin family (190 files — the complete test family for this PR's entire file surface; every edit is inside that directory): 'Test Files 190 passed (190) · Tests 1955 passed | 1 skipped (1956)', lock 'VERDICT command-exit 0 · held the lock 581s (9m41s) · waited 40s'. This took two attempts: the first returned exit 99 (queue-timeout, never acquired after 9m00s) and was reported as NOT RUN rather than claimed; the retry acquired it. || GATES: check:control-bytes '✅ OK (scanned 4585 tracked text file(s); skipped 85 binary)'; check:esm-specifiers 'Specifier leg: no un-ledgered package emits an extensionless relative specifier' (the family this diff most directly implicates — it added two ./form-spec.js specifiers); check:self-import '✅ No package names itself inside its own src/'; check:phantom-deps '✅ Every in-scope import is declared by the package that publishes it'; check:spec-symbols '✅ spec symbol derivation: 1285 files scanned against 4912 spec export names'. check:published-dist and the full check:node-esm-load load leg are NOT owed by this diff — measured from their workflows' `on:` blocks, both trigger only nightly, on workflow_dispatch, or on a push touching the gate's own script. || LINT, scope stated so it is checkable: `pnpm lint` is `turbo run lint`, i.e. each package's own `eslint .`; the job for the only package this PR edits was run in full — exit 0, '✖ 2508 problems (0 errors, 2508 warnings)' (all pre-existing no-explicit-any warnings). Population read from eslint's own config, not guessed: 898 files from `--format json`, 0 errors, and all five changed files verified present in that population by filePath. No type-aware linting is configured (no `project` / `projectService` in eslint.config.js), so an app-shell edit cannot move the verdict on a file in a package this PR does not touch. || REVERSE VERIFICATION — prediction written to disk BEFORE the run (timestamped 2026-08-21T09:00:41Z). Ablation: delete only `dependsOn` from FormFieldSpec, keep everything else. NO REBUILD NEEDED OR PERFORMED, and this is stated rather than skipped: tsc reads app-shell src/ directly, and the only thing tsconfig.test.json resolves through built .d.ts is OTHER packages, which the ablation does not touch — so no stale dist can be credited for either colour. MUTATION CONFIRMED ON DISK before anything was read, anchored at the exact text removed: declaration lines matching '^ dependsOn?: string | string[];$' went 1 -> 0 and `git diff --numstat` showed 0/25 on that file — not a bare non-empty diff. The script carried `trap '<git checkout>' EXIT INT TERM`; restoration was verified by an empty `git status --porcelain` for that path afterwards. PREDICTED vs OBSERVED: (1) source project goes red — widgets.tsx now reads dependsOn OFF FormFieldSpec, TS2339, >=3 sites -> OBSERVED 4x TS2339 at widgets.tsx:670 and :2411 (x3). This is the card in one line: those same reads COMPILED before this PR, because they were checked against widgets.tsx's own copy. (2) test project goes red with the card's exact error -> OBSERVED TS2353 on PIN A/B and the three typed runtime specs, plus SchemaForm.widgetLabelling.test.tsx x3. (3) both @ts-expect-error negative controls stay SATISFIED, no TS2578 -> OBSERVED, no TS2578 anywhere. (4) vitest stays GREEN because the widgets read the key at runtime regardless -> OBSERVED 10/10 passed with the type removed, which is why a runtime-only verification of this card would have proved nothing. TWO HONEST DELTAS: (a) UNPREDICTED, same direction — DynamicConfigWidget.test.tsx also went red (5x TS2353); it passes fieldSpec={{…, dependsOn}} as an object literal, so the convergence pulled a PRE-EXISTING test under the authoring type's checking. More diagnostics, not fewer. (b) PIN E failed as TS2339 on the indexed access, not the TS2344 I predicted — same direction, different code. || NEGATIVE CONTROLS (the card's explicit requirement that the pin is not merely 'the type got looser'), both @ts-expect-error so a LOOSENING turns the file red via TS2578: PIN C — an undeclared key is still rejected, so the fix is not an index signature or `any`; PIN D — `dependsOn: [{ field, param }]` is still rejected, pinning the DependsOnInput trap as a refusal in the tree rather than as prose in a comment. Plus PIN E (exact shape string|string[]|undefined), PIN F (WidgetProps['fieldSpec'] IS FormFieldSpec, so a re-inlined copy fails even if it agrees on every key), PIN G (the leaf and SchemaForm's re-export are the same type).", "open_questions": [], "out_of_scope_findings": [ "filed as #5542 (unassigned, `finding`, no `pm:queue`): a THIRD inline copy of this contract at apps/console/src/components/FormPage.tsx:278 — the card and triage both frame this as TWO descriptions converging; measured, it is three. apps/console is held by in-flight #5504 and was outside this card's declared surface, so it is untouched here and PR #5537 says in its scope section that this card lands the app-shell half only. Searched open issues before filing (semantic + keyword, FormFieldSpec / FormPage.tsx / console-duplicate phrasings) — no card covered it. The body deliberately leaves open whether the right answer is 'merge' or 'declare them distinct and rename one', since apps/console may legitimately author a narrower shape.", "NOT FILED, PM TOOLING, needs your call on where it belongs: `scripts/pm/dispatch-gates.mjs` resolves its repo root from its OWN location (fileURLToPath(import.meta.url)), not from cwd. Run from the objectui worktree as the dispatch instructed, it derived the changeset from the OBJECTSTACK tree and reported 'this branch changes nothing against origin/main (merge base 76deca249)' — 76deca249 is an objectstack commit. It exited 2 and refused rather than printing a wrong gate list, which is the good failure mode, but it means the mandated 'don't trust the dispatch gate list, re-derive it' step is UNAVAILABLE for any objectui card. I derived the families by hand from objectui's own .github/workflows/*.yml instead, and said so in the PR.", "NOT FILED, observation only, no action taken per basic rule 2: editing the PR body through the API appended a SECOND signature footer in the BARE form (`https://claude.ai/code` with no session id) after my session-URL one. It was not appended on create, only on the PATCH. My session-URL footer survived both paths exactly as documented. I did not remove the added line — it is not mine to correct, and the bare form self-strips on the next edit anyway." ] }
Generated by Claude Code
Generated by Claude Code
- added a commit that references this issue
on Aug 21, 2026 - added a commit that references this issue
on Aug 23, 2026
发现于 #4871 实施时的
tsc -p tsconfig.test.json(基线167ec42e7)。未认领,交 PM triage。机制
packages/app-shell/src/views/metadata-admin/SchemaForm.tsx导出的FormFieldSpec是 form 布局的授权面(section 的fields[]元素类型)。它声明了type/options/reference/multiple/widget/language等等,但没有dependsOn。而
widgets.tsx里两个 widget 把dependsOn当必备配置读:FieldSelectorWidget(widget: 'field-selector'):fieldSpec?.dependsOn || fieldSpec?.reference || 'objectName'—— 决定去哪个对象拉字段目录;DynamicConfigWidget(widget: 'dynamic-config'):Array.isArray(fieldSpec?.dependsOn) ? fieldSpec!.dependsOn[0] : fieldSpec?.dependsOn—— 决定用哪个兄弟字段的值去查context.dynamicSchemas。它们读得到,是因为
WidgetProps.fieldSpec是另一个内联结构类型(widgets.tsx :101 起),那个声明了dependsOn?: string | string[]。两个类型描述同一个对象、由MetadataField把同一个值传下去,却对这个键不一致。后果:用
FormFieldSpec类型化地写 form spec 时,{ field: 'fieldName', widget: 'field-selector', dependsOn: 'objectName' }是编译错误(TS2353: 'dependsOn' does not exist in type 'FormFieldSpec'),而这恰恰是让field-selector工作的唯一方式。当前仓内 spec 大多以as any/ 松类型经过,所以没被挡住 —— 这也是它至今没暴露的原因。定级说明
按「观察类」提交(
finding标签,无pm:queue):今天没有用户点得到的失效面 —— 运行期读的是同一个对象,dependsOn传得到;坏的是授权时的类型面。但方向就是本仓反复付过学费的那一类:producer 与 consumer 对同一个契约各留一份不一致的描述。严重度请 PM triage 判,不由我在立卡时定。建议
WidgetProps.fieldSpec那份内联结构与FormFieldSpec明显是同一个契约的两次描述(前者还额外有colSpan/immutable/readonly/required等重叠键)。合并成一处,WidgetProps.fieldSpec直接引FormFieldSpec,是契约优先的做法;dependsOn补进FormFieldSpec是其中一步。合并会牵动两个文件的导入方向(widgets.tsx目前不从SchemaForm.tsx引类型,反向引会成环),所以形态需要先定,不宜顺手做。边界与去重
FormFieldSpec dependsOn missing type metadata-admin field-selector dynamic-config),无同题单。Label htmlFor={id},但 17 个注册 widget 里只有 5 个消费 id —— 分组类控件面的for悬空且组无可访问名 #4871:那单是 labelling 声明层与命名通道,已实施;本单是fieldSpec契约的两份不一致描述,与 labelling 无关。