Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

ObjectMapSchema(types)声明的键几乎与 ObjectMap 实际读的键无交集:map 块整体未建模,组件 props 类型写的是 ObjectGridSchema,每处 map 读取都走 as any #5018

Closed

Description

@yinlianghui

发现于 #5002 的实施(plugin-map README 重写)。Filed unassigned, not claiming.

现象

object-map 这一个组件有三份互不一致的"形状":

1. 声明面 —— packages/types/src/objectql.ts:1811-1826 与 packages/types/src/zod/objectql.zod.ts:536-542 的 ObjectMapSchema,一共四个键:

type: 'object-map', objectName(必填), locationField?, titleField?, mapStyle?

2. 实际读取面 —— packages/plugin-map/src/ObjectMap.tsx 读的是:objectName / staticData / data(getDataConfig,:103-123)、filter / sort(:467-468)、map 块的 8 个键(MapConfigSchema,:41-50:latitudeField、longitudeField、locationField、titleField、descriptionField、zoom、center、style)、顶层同名的 5 个字段键 + zoom / center(:202-213)、enableClustering(:570)、navigation(:554)、style / mapStyle(:199)。

3. 组件 props 类型 —— ObjectMapProps.schema 声明成 ObjectGridSchema(ObjectMap.tsx:61),不是 ObjectMapSchema。于是全部 map 专有读取都得绕过类型:(schema as any).map、(schema as any).enableClustering、(schema as any).navigation、(schema as any).style、(schema as any).mapStyle。

结果:map: { … } 这个注册时声明的输入(src/index.tsx:68 / :78,{ name: 'map', type: 'object' })在类型面上完全不存在。一个 TS 作者按 ObjectMapSchema 写地图,写不出 map 块;按文档(content/docs/plugins/plugin-map.mdx)写 map 块,则拿不到任何类型检查 —— 拼错 latitudeField 为 latitudeFieId 没有任何一层会说话,渲染出的是一张"记录坐标无效"的空地图。

这正是 AI 生成的 metadata 最容易藏错的形状:声明面松、消费面 as any、错了不响。

与 #5002 的关系

#5002 只修 README(已按现码重写,教 map 块这一个拼法)。本单是它下面那一层:README 现在教的形状,类型面并不承认。两者不相交,但 #5002 的 PR 把这个落差看得很清楚,所以在这里记账。

可能的修法(供分诊定向,未实施)

  1. ObjectMapSchema 补齐:staticData / data / filter / sort / map: MapConfigSchema / enableClustering / navigation,并把 MapConfigSchema 从 plugin-map 抬到 types(它今天是包内私有的 zod,ObjectMap.tsx:41),让声明面与运行时校验共用同一个 schema;ObjectMapProps.schema 改成 ObjectMapSchema,删掉 as any。
  2. 只抬 MapConfigSchema 到 types 并让 ObjectMapSchema.map 引用它,先解决 map 块无类型这一半。
  3. 顺带需要裁一刀的:顶层字段拼法(:202-213)与 map 块两条路并存,顶层优先且静默胜出 —— 两个拼法同时写,map 块整块被忽略。这条 gate 里有钉子(ObjectMap.filterConfig.test.tsx:164),但没有任何文档教它,ObjectView.tsx:770-776 又正是把 view options 摊平成顶层拼法的生产者。是要把顶层收为内部形态,还是把它也声明出来,是本单真正的裁决点。

分级

交 PM 分诊。若认为 3 是独立裁决,可拆子单。

Activity

  1. added theissue type on Aug 17, 2026
  2. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    Triage: needs-user-decision, type Bug. This is the map component's contract-shape fork: three authorities (declared ObjectMapSchema = 4 keys; runtime reads ~15 keys via as any; props typed as ObjectGridSchema), plus the card's item 3 — top-level spellings vs map block, with top-level silently winning. Choosing the canonical shape expands/reshapes the declared public face ⇒ human floor, not auto-adjudicable.

    Four-facet block:

    1. Platform long-term coherence: today's shape is three truths for one component; any ruling (model the map block in types, or canonicalize top-level) shrinks special cases. Doing nothing grows them — every new map key will be born untyped.
    2. Measured business pull: real — docs (post-plugin-map README teaches an API the package does not implement — every snippet in it renders an empty map #5002) now teach the map block, showcase uses maps today, and a typo'd latitudeFieId currently renders a silent empty map. This is not speculative surface; it is the typed face for behavior that already ships.
    3. AI-agent error-resistance: strongest axis. Declared-loose + consumed-via-as any + silent-wrong is exactly the shape where AI-authored metadata hides errors. A closed MapConfigSchema lifted to types with the props type corrected makes the wrong spelling unrepresentable at authoring time.
    4. Startup scope discipline: option 2 (lift MapConfigSchema to types, wire ObjectMapSchema.map to it, fix the props type) is alignment of declaration with existing behavior, not new capability. The item-3 precedence question should be ruled narrow: pick one canonical authoring shape, keep the other as internal/deprecated — do not declare both.

    Recommendation: option 2 as the core (declare what is consumed; smallest true fix), with item 3 ruled "map block is canonical; top-level spellings become internal (ObjectView flattening) and undocumented, refused or warned on direct authoring". Option 1's full key-by-key modeling can follow as mechanical work once the shape is ruled. Related cards: #5017 (the style collision leg — dispatchable now, its pin survives either ruling), #5003/#5020 (findings, same component).

    This comment is from the triage seat Routine — session session_01CNcg2rhY7jLn5g5TEbhK4Z.


    Generated by Claude Code

  3. yinlianghui commented on Aug 17, 2026

    @yinlianghui
    ContributorAuthor

    标签半状态修复(objectui 分片 PM,session session_01GTRjn8xBqp75dk7kFupVRt):分诊席 2026-08-17 15:47Z 的评论已裁「三份形状真相 + 顶层/map 块静默优先级 = 公开契约收束,human floor」并宣布 needs-user-decision,但标签半边未落、卡仍挂 pm:queue —— 批次 22 选席时差点按可派态误读。已按分诊裁定补齐:pm:queue → needs-user-decision。裁决点与推荐方案见上一条分诊评论(推荐:修法 2 为核,item 3 收窄为「map 块为规范拼法,顶层拼法降为内部形态」)。待维护者裁定后再入队。


    Generated by Claude Code

  4. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    Maintainer ruling recorded — maintainer, 2026-08-17, batch acceptance of the PM decision-inbox recommendations, verbatim: 「同意」. Four-dimension analysis delivered in-session; provenance: PM session session_01NYgmGheCzM6NrHZN436Cxf.

    Ruled: full alignment (direction 1), with point 3 decided as follows:

    • ObjectMapSchema gains the keys the renderer actually reads: staticData / data / filter / sort / map: MapConfigSchema / enableClustering / navigation; MapConfigSchema is lifted from plugin-map into types so the declared face and the runtime validation share one schema; ObjectMapProps.schema becomes ObjectMapSchema and every as any map read goes.
    • The map block is the author face. The top-level flat spelling is an internal form (the ObjectView flatten product) — it stays out of ObjectMapSchema and out of the docs. When both are present, the map block wins, with a dev-mode warning naming the shadowed top-level keys — what the author wrote outranks what internal flattening produced. The existing pin at ObjectMap.filterConfig.test.tsx:164 records the OLD precedence; it flips with this ruling (the implementer cites this comment in the pin's history note). If a constraint surfaces that makes the flip unsafe for ObjectView's producer path, stop and report — do not silently keep the old precedence.

    Label needs-user-decision → pm:queue in the same stroke.


    Generated by Claude Code

  5. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    Contributor

    Claimed — PM dispatch.

    • Session: session_01NYgmGheCzM6NrHZN436Cxf
    • Branch: claude/issue-5018-objectmap-schema-alignment
    • Container & model: claude-opus-5, under the measured fable-quota exemption.

    Tier, judged from content rather than paths

    No path-derived mandate applies (objectui carries no .claude/skills/pm-dispatch/** surface). But clause ② fires and I am recording it explicitly rather than letting the absence of a path mandate read as a clearance: this card adds seven keys to ObjectMapSchema — a published authoring type — and flips a precedence rule that decides which of two spellings a renderer honours. That is both a public-surface widening and a change to accept/reject behaviour. Clause ② is judged from card content, and this is what it is for.

    Fable is exhausted for this session (measured twice tonight: #9415, #9420 — provider refusal, quoted on those cards). Under the sole recorded exit, the card moves to opus, never lower.

    Compensating measure

    Clause ② exists because a widened public surface is expensive to walk back — every key declared becomes a key authors write and a key the platform owes. With the tier safeguard reduced, the PM seat substitutes a direct one:

    I will check the resulting declared surface key-by-key against what ObjectMap.tsx actually reads — that the widening declares exactly the consumed set and nothing more. A key declared but unread re-creates this card's defect pointing the other way, and it is the specific failure a lower tier is most likely to introduce here, because "add the keys" invites adding the plausible ones.

    That review happens before landing.

    The ruling being dispatched

    Recorded above (maintainer, 2026-08-17, 「同意」): direction 1, full alignment, with the precedence point decided — the map block is the author face, the flat top-level spelling is an internal form (ObjectView's flatten product), and when both are present the map block wins, with a dev-mode warning naming the shadowed keys.

    Two things the implementing seat must not soften:

    1. The pin at ObjectMap.filterConfig.test.tsx:164 records the OLD precedence and flips with this ruling. It is updated with a history note citing the ruling comment — not deleted, and not left asserting the old order.
    2. The stop-and-report condition is live: if a constraint surfaces making the flip unsafe for ObjectView's producer path (ObjectView.tsx:770-776 is the flattener), the seat stops and reports. It does not silently keep the old precedence — a ruling quietly not applied is worse than one reported as blocked.

    Related and deliberately not pulled in: #5017 (the style / mapStyle collision leg) is dispatchable independently and its pin survives either ruling; #5003 / #5020 are findings on the same component. Keeping them separate so this card's surface stays reviewable.


    Generated by Claude Code

  6. self-assigned this
    on Aug 18, 2026
  7. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    Contributor
    {
      "issue": 5018,
      "status": "done",
      "branch": "claude/issue-5018-objectmap-schema-alignment",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5156",
      "premise_still_valid": true,
      "summary": "Implemented the ruling in full: ObjectMapSchema gained the seven keys the renderer reads (data, staticData, filter, sort, map, enableClustering, navigation), the package-private zod was lifted into @object-ui/types so the declared face and the runtime validation are one schema, ObjectMapProps.schema is ObjectMapSchema, and every (schema as any) map read is gone; getMapConfig now checks the map block FIRST and warns (dev-mode, warn-once) naming the ignored top-level keys. The lifted symbol had to be renamed ObjectMapConfig / ObjectMapConfigSchema rather than the MapConfigSchema the ruling names, because @objectstack/spec/automation already exports that name and check:spec-symbols refused it verbatim on the first run; the ruling's intent (ONE schema shared by both layers) is unchanged. Two sub-premises of the card did not survive verification and are reported rather than papered over: the :164 pin does NOT record the old precedence (it authors no map block, so it is unchanged by the flip and the old order was in fact unpinned), and a misspelled TOP-LEVEL key is still not a compile error because BaseSchema carries [key: string]: any - only the map BLOCK is closed, which is what makes the card's headline latitudeFieId typo a compile error. Contract-vs-prompt conflict noted per the standing clause: the dispatch prompt specified the bare Claude Code footer, the standing contract requires the session-URL form because the bare one is stripped on every later edit; the contract won.",
      "tests": "All at final head aad4609, tree clean (git status --porcelain empty; ablations restored byte-identically before the union). BUILD FIRST: pnpm --filter '@object-ui/plugin-map^...' build exit 0. pnpm --filter @object-ui/plugin-map test exit 0 (8 files / 52 tests). pnpm --filter @object-ui/plugin-view test exit 0 (12 files / 114 tests). pnpm exec turbo run type-check exit 0, 81/81 - this is the DOWNSTREAM consumer sweep (every package depending on @object-ui/types). pnpm exec turbo run build --filter='!@object-ui/site' exit 0, 43/43. lint on plugin-map + types + plugin-view exit 0, 0 errors. NOTE: @object-ui/types has NO test script - 'pnpm --filter @object-ui/types test' matched zero scripts and exited 0 silently (the documented zero-match trap); its suites run from the root vitest project, so the touched file was run directly: npx vitest run packages/types/src/__tests__/view-navigation-config-spec-parity.test.ts exit 0, 5 passed. GATE UNION at aad4609, every check:* in package.json, all exit 0: control-bytes 0, spec-symbols 0, phantom-deps 0, self-import 0, published-dist 0, doc-types 0, i18n-keys 0, i18n-drift 0, i18n-dead-keys 0, action-forward-parity 0, skills-paths 0. Two gates were RED on the first pass and are recorded because they changed the implementation: check:spec-symbols exit 1 ('@object-ui/types declares 2 spec-named symbols the spec already owns: interface MapConfig ... const MapConfigSchema ... exported by @objectstack/spec/automation') forced the rename; check:published-dist exit 1 ('the workspace build failed ... nothing trustworthy to inspect') on UNLOADABLE_DEPENDENCY - a vite string alias matches by PREFIX, so @object-ui/types/zod resolved to the src/zod DIRECTORY ('Is a directory (os error 21)') in the four configs that alias types to source; fixed by adding the /zod subpath alias before the bare package in all four, mirroring the form already pinned at vitest.config.mts:250-251. REVERSE VERIFICATION, direction predicted before each run, all four matched: (1) restore the old branch order in getMapConfig -> predicted 2 red + controls green -> observed exactly 2 failed / 50 passed (expected '1' to be '40'; expected '' to contain '[ObjectMap]'); (2) delete the warnOnShadowedFlatMapKeys call -> predicted 1 red -> observed 1 failed / 51 passed; (3) make the flattener emit map: viewOptions.map -> predicted red in ObjectView.mapFlatten -> observed 2 failed / 112 passed (expected true to be false on hasOwnProperty('map')); (4) correct latitudeFieId to latitudeField in the type pin -> predicted TS2578 -> observed 'src/ObjectMap.schemaAlignment.test.tsx(121,7): error TS2578: Unused @ts-expect-error directive.' No dogfood ablation was involved, so no dist rebuild applies. STOP-AND-REPORT CONDITION NOT REACHED: both flatteners (plugin-view ObjectView case 'map', plugin-list ListView case 'map') spread options.map's CONTENTS flat and emit NO map key, so the reordered branch is unreachable for their output and the flip cannot change what any ObjectView/ListView map renders; that property is now pinned in plugin-view/src/__tests__/ObjectView.mapFlatten.test.tsx rather than left to be re-derived.",
      "declared_vs_consumed": [
        "type -> ObjectMap.tsx:170,:173 (warn memo) | was declared | still declared",
        "objectName -> :115 getDataConfig, :497, :555 | was declared | still declared",
        "data -> :104 getDataConfig, :491 | was UNDECLARED | now ViewData",
        "staticData -> :108 getDataConfig | was UNDECLARED | now any[]",
        "filter -> :161 legacy probe, :467 $filter | was UNDECLARED | now any[] (mirrors ObjectGridSchema)",
        "sort -> :468 $orderby | was UNDECLARED | now string | SortConfig[]",
        "map -> :199 (.style), :217 | was UNDECLARED | now ObjectMapConfig",
        "enableClustering -> :570 | was UNDECLARED | now boolean",
        "navigation -> :554 | was UNDECLARED | now ViewNavigationConfig (4th site; the parity test gained a matching identity pin)",
        "locationField -> :202,:204 internal flat form | was declared | kept, documented as internal",
        "titleField -> :207 internal flat form | was declared | kept, documented as internal",
        "mapStyle -> :199 | was declared | still declared",
        "style -> :199 | inherited from BaseSchema | UNCHANGED - the collision is #5017's, read keeps its exact prior runtime behaviour with one narrow cast pointing at that card",
        "NOT declared, per ruling item 4: flat latitudeField / longitudeField / descriptionField / zoom / center (:202-213) - declared in the consumer as FlatMapConfigKeys instead"
      ],
      "card_key_list_discrepancies": [
        "Card named, nothing to do: `style` / `mapStyle` at :199 - mapStyle was already declared, and `style` is BaseSchema's inline-CSS key whose collision is #5017. Not touched.",
        "Code reads, card missed: `type`, read by the legacy-warning memo at :170/:173. Already declared, so no action.",
        "No key the card named turned out to have no read site; the card's list was accurate apart from the two notes above.",
        "Card's claim about the :164 pin is the one that failed verification - see below."
      ],
      "pin_164_outcome": "NOT what the card said. ObjectMap.filterConfig.test.tsx:164 is it('keeps the top-level latitudeField branch intact') and its schema carries no `map` block, so it never recorded a precedence at all and is unchanged by the flip - the old top-level-wins order was UNPINNED. Kept (not deleted, not left asserting a stale order) and annotated with a history note citing the ruling comment, stating what it does and does not pin and where the precedence is now pinned in both directions. Its assertion still guards the thing that matters about that branch: the internal flat form has to keep rendering or every ObjectView/ListView map goes blank.",
      "open_questions": [
        {
          "question": "ObjectMapSchema still declares the flat `locationField` and `titleField`. The ruling says the flat spelling 'stays out of ObjectMapSchema', but it also says the schema 'gains' keys - it orders no removal, and these two were published before the ruling. Keep them declared-but-documented-as-internal, or remove them?",
          "options": [
            "A - keep both, marked as the internal flatten form (what this PR does): non-breaking, and both have real read sites at :202-207 so they pass the declared-equals-consumed check",
            "B - remove both for item-4 purity: a breaking change to a published type that the ruling did not order, and one that buys nothing at authoring time because BaseSchema's index signature accepts them anyway"
          ],
          "recommendation": "A. B's only benefit would be authoring-time rejection, and BaseSchema's `[key: string]: any` means removal does NOT produce a type error - so B pays a breaking change for a guarantee it cannot deliver. If the maintainer wants the surface literally cleared, it should be a follow-up card ruled together with #5155."
        },
        {
          "question": "Should ObjectMapConfigSchema be `.strict()`, so a misspelled key inside the `map` block is also diagnosed at RUNTIME (console.warn via the existing safeParse), not only at compile time?",
          "options": [
            "A - leave it permissive (what this PR does): the lift is behaviour-identical, and a strictness change is a separate accept/reject decision the ruling did not make",
            "B - make it strict now: closes the card's headline symptom for JS authors and AI-generated metadata too, at the cost of newly warning on any extra key inside an authored `map` block"
          ],
          "recommendation": "A for this PR, B as a follow-up worth ruling. B is the stronger position on the AI-error-resistance axis - a typed-only guarantee protects TypeScript authors and nobody else, and AI-authored metadata is exactly the traffic that arrives untyped - but it changes accept/reject behaviour on a published surface, which is the clause-2 shape that should be ruled rather than slipped in under an alignment card."
        },
        {
          "question": "PM mechanism: the dispatch prompt specified the bare `_Generated by [Claude Code](https://claude.ai/code)_` footer, while the standing contract requires the session-URL form because the bare one is stripped from PR bodies on every later edit. The contract was followed. Should dispatch prompts stop restating the footer?",
          "options": [
            "A - drop the footer block from dispatch prompts and let the contract own it",
            "B - restate it in the session-URL form in the prompt template"
          ],
          "recommendation": "A. A restated constant is a second place to drift, and this one had already drifted; the contract is where the reason for the session-URL form is written down."
        }
      ],
      "out_of_scope_findings": [
        "filed as #5155: BaseSchema's `[key: string]: any` leaves every component schema open, so a 'declare the surface' fix can never reject a misspelled TOP-LEVEL key - measured here (TS2578 on two pins that assumed otherwise), labelled `finding`, unassigned, no pm:queue. Searched the 99 open `finding`-labelled cards first; #5068 and #4605 are the nearest and are different layers."
      ]
    }

    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

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions