Skip to content

DraftChangesPanel interpolates the stored type verbatim into /meta item routes — fold to singular before interpolating (step ③ residue of objectstack#9180) #5356

Description

@os-zhuang

Filed at destination by the objectstack spec seat carving the objectstack-ai/objectstack#9180 ruling, under the maintainer's direct reassignment (2026-08-19, verbatim: 「改派给 spec 席,现在就拆」; audit: objectstack-ai/objectstack#9180 comment 5349687344).

The ruling being implemented (⛔ not re-litigable)

The /meta type segment is singular — always (objectstack-ai/objectstack#9180, maintainer 2026-08-16). The 2026-08-17 re-weigh scoped step ③ to internal spelling correction only across the three self-owned repos, verbatim, untranslated: 「② 照原样做;只需要修正 objectstack objectui cloud 中错误的写法。」

Measured sites (2026-08-19, objectui origin/main — re-verify at your branch point)

packages/app-shell/src/preview/DraftChangesPanel.tsx:

  • :62 — type: d.type as string (taken straight from the /api/v1/meta/_drafts feed's stored rows; typeof check only, no fold)
  • :76 — fetch(`/api/v1/meta/${encodeURIComponent(type)}`)
  • :112 — `/api/v1/meta/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`

The panel is data-driven, not authored: a stored plural row makes the Console emit a plural request. The #9180 sweep found this is the only variable-typed /meta item-route emitter in objectui (zero literal plural routes; 29 files with singular routes as the positive control).

The fix shape

Fold/normalize the stored type to the singular spelling before interpolating into any /meta route (a local normalization at the panel boundary; pre-#7894 at-rest plural residue stays unreachable and is not laundered by this). Internal correction only — nothing about what the server accepts changes.

Dedup: objectui search_issues is known-broken (returns 0 for every query); checked via list_issues title scan instead — no existing card names DraftChangesPanel or /meta spelling.

Part of objectstack-ai/objectstack#9180.

Activity

  1. os-support-ai commented on Aug 20, 2026

    @os-support-ai
    Collaborator

    Claim: PM loop round 18
    Session: session_01RV6yuVCxymHYE16PL9vQkE
    Branch: claude/issue-5356-draftchanges-meta-singular
    Worktree: objectui-issue-5356
    Domain: repo:objectui (execution seat)
    File surface: packages/app-shell/src/preview/DraftChangesPanel.tsx and its test (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus — node scripts/pm/dispatch-gates.mjs --tier <paths> returns no path-derived mandate; tier is this seat's call.
    Clause-②: no — internal spelling correction on an outbound route; nothing about what the server accepts changes, per the card and the ruling it carves.
    Serial constraints cleared: packages/app-shell/src/preview/ has no in-flight claim. The app-shell subdirectory-level serial exemption applies and does not spill: the last app-shell PR to land (PR5355, #5228) was in src/views/metadata-admin/, and #5357 (app-shell ESM specifiers) is pm:blocked, not dispatched. Round-18 siblings hold disjoint surfaces.

    Premise re-verified on origin/main bdf8cf76e, 2026-08-20 — all three sites present at the stated lines (:62 type: d.type as string, :76 and :112 the two interpolations), plus :49 /api/v1/meta/_drafts as the feed they come from.

    The ruling is not re-litigable. objectstack-ai/objectstack#9180, maintainer 2026-08-16: the /meta type segment is singular, always. Step ③ was scoped by the 2026-08-17 re-weigh to internal spelling correction across the three self-owned repos only, verbatim, untranslated: 「② 照原样做;只需要修正 objectstack objectui cloud 中错误的写法。」 You implement it; you do not re-open it.

    What "fold to singular" has to mean here

    The panel is data-driven, not authored — type arrives from the /api/v1/meta/_drafts feed's stored rows with only a typeof check. So this is not a find-and-replace of a literal: it is a normalization at the panel boundary, applied before the value reaches any /meta item route.

    Two things to get right, and both are places where a plausible-looking fix is wrong:

    1. Fold, don't strip. Whatever helper you use must be the repo's existing singular-folding rule if one exists — look for it before writing a new one, and say in the PR which you found and why you used it. A hand-rolled replace(/s$/, '') is wrong for any type whose singular already ends in s. If no shared helper exists, say so explicitly; do not silently invent one and present it as the convention.
    2. Do not launder the at-rest data. Pre-docs(app-shell): page-route action examples taught the unreadable nested action bag #7894 plural residue stored on the server stays unreachable; this change makes the Console stop emitting plural, it does not migrate anything. ⛔ Do not add a write path, a migration, or a server-side tolerance.

    Reverse-verification

    Pin the behaviour, then revert your normalization and show the pin goes red — a route-spelling fix is exactly the kind that passes vacuously if the fixture feeds an already-singular type. State whether a build artifact sits between your edit and the thing under test, per leg.

    Part of objectstack-ai/objectstack#9180 — ⛔ not Fixes for the objectstack card; use Fixes #5356 for this one.


    Generated by Claude Code

  2. os-support-ai commented on Aug 20, 2026

    @os-support-ai
    Collaborator

    os-dev-report

    {
      "issue": 5356,
      "status": "done",
      "branch": "claude/issue-5356-draftchanges-meta-singular",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5361",
      "premise_still_valid": true,
      "summary": "Re-verified on origin/main bdf8cf76e: all four measured sites present at the stated lines, and :76/:112 are still the only variable-typed /meta route emitters in objectui (independent grep: every other site interpolates a name into a literal singular type). The stored `type` is now folded once, at the boundary where _drafts rows become DraftChangeEntry, with @objectstack/spec/shared's canonicalMetaUrlType — the /meta-specific fold (objectstack#7894/#8424), not pluralToSingular, whose keys are defineStack() collection properties and which is missing field/seed/external_catalog/translation. Both item routes, the grouping and the type heading read that one folded value. Emit-side only: no write path, no migration, no server-side tolerance. DESIGN NOTE THE PM SHOULD SEE: my first implementation mirrored the 35-entry table locally with a parity test, to avoid the measured +213.4 KB min / +60.1 KB gz this import adds to the console's EAGER graph (the panel is static from ConsoleLayout, and the vendor-objectstack chunk group makes a dynamic import ineffective — objectui#5266). scripts/check-spec-symbol-derivation.mjs refuses that mirror by design (a spec-named symbol must be derived; a faithful copy is the fork it exists to prevent), so the import is the sanctioned form and the weight is filed as #5359, whose levers live in apps/console/vite.config.ts — outside this card's file surface. File surface honoured: only DraftChangesPanel.tsx, its test, and the changeset.",
      "tests": "All from the repo root at 2efc90d19, heavy runs under flock /tmp/os-heavy-verify.lock. (1) `pnpm exec vitest run packages/app-shell/src/preview/__tests__/DraftChangesPanel.test.tsx` -> 'Test Files 1 passed (1) / Tests 13 passed (13)' (8 pre-existing + 5 new route pins). (2) `pnpm --workspace-concurrency=2 --filter @object-ui/app-shell type-check` -> exit 0 with the script echoed (`tsc --noEmit && tsc -p tsconfig.test.json`), after `pnpm --filter '@object-ui/app-shell^...' build`; before that build it failed with TS2307 on every workspace dep, i.e. the run was real, not a zero-match pass. (3) eslint on both changed files -> 0 errors, 2 pre-existing warnings on untouched lines. (4) Gates at this sha, all exit 0: check-spec-symbol-derivation, check-control-bytes, check-changeset-presence, check-changeset-no-major, check-phantom-dependencies, check-package-self-import. REVERSE VERIFICATION: reverting the fold to `type: d.type as string` turned 4 of the 5 route pins red -- \"expected [ '/api/v1/meta/objects' ] to include '/api/v1/meta/object'\", \"expected [ '/api/v1/meta/capabilities' ] to include '/api/v1/meta/capability'\", \"expected 'objects · 1' to be 'object · 1'\" -- and restoring returned 13/13. The 5th pin (already-canonical type + plugin kind pass through) is green in both legs by design; it is the passthrough control. NO BUILD ARTIFACT SITS BETWEEN THE EDIT AND THE SUBJECT ON EITHER LEG: the test imports ../DraftChangesPanel by relative path and vitest transforms it from source, so neither leg needed a rebuild; the only prebuilt artifact in play is the installed @objectstack/spec dist that provides the fold, which the ablation does not touch. Ran the ablation from the committed state, so both readings are reproducible from git.",
      "open_questions": [
        {
          "question": "The fix knowingly adds +60.1 KB gzipped to every console page load (measured) to fold a route segment an admin-only panel emits. Ship as-is, or pay it down?",
          "options": [
            "A - Ship as-is (this PR). The spec-derivation gate refuses a local mirror; the weight is filed as #5359 and recorded at the import site so it is not invisible.",
            "B - Follow up on #5359: exclude the spec `shared` subpath from VENDOR_OBJECTSTACK_TEST and switch this fold to `await import()` inside the async boundary that already exists, with a build assertion in the spirit of assertLazyLinterStaysLazy. Both halves are in apps/console/vite.config.ts, outside this card's surface.",
            "C - Ask upstream for a finer-grained spec export for the /meta spelling contract, which would make the trade disappear for every consumer."
          ],
          "recommendation": "A now, B or C as a separate card. A is the only option inside this card's file surface, and it is the one the repo's own gate sanctions; the bytes are recorded rather than silent, which is what #5324 says the console's budget cannot do on its own."
        }
      ],
      "out_of_scope_findings": [
        "filed as #5359: the /meta fold puts @objectstack/spec/shared on the console's eager graph (+213.4 KB min / +60.1 KB gz, measured); the vendor-objectstack chunk group makes lazy loading ineffective, and no budget weighs those bytes (related: #5324, #5325, objectui#5266)",
        "not filed, already tracked: #4940 covers the sibling family in data-objectstack (adapter meta call sites passing a plural type argument through the client). Out of this card's file surface; no duplicate opened. Note for the PM: the card's premise says objectui has 'zero literal plural routes', which my own grep confirms for raw fetch URLs -- #4940's sites are plural TYPE ARGUMENTS handed to the metadata client, not literal plural URLs, so the two statements do not conflict."
      ]
    }

    Generated by Claude Code

  3. os-support-ai commented on Aug 20, 2026

    @os-support-ai
    Collaborator

    os-dev-report

    Supersedes the report in comment 5350067829 — same delivery, with the PM's subpath-reachability question answered (tests and open_questions updated; no code change, still 2efc90d19).

    {
      "issue": 5356,
      "status": "done",
      "branch": "claude/issue-5356-draftchanges-meta-singular",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5361",
      "premise_still_valid": true,
      "summary": "Re-verified on origin/main bdf8cf76e: all four measured sites present at the stated lines, and :76/:112 are still the only variable-typed /meta route emitters in objectui. The stored `type` is folded once, at the boundary where _drafts rows become DraftChangeEntry, with @objectstack/spec/shared's canonicalMetaUrlType — the /meta-specific fold (objectstack#7894/#8424), not pluralToSingular, whose keys are defineStack() collection properties and which is missing field/seed/external_catalog/translation. Both item routes, the grouping and the type heading read that one folded value. Emit-side only: no write path, no migration, no server-side tolerance. The import costs a measured +213.4 KB min / +60.1 KB gz on the console's EAGER graph; a local mirror of the table was tried first and is refused by scripts/check-spec-symbol-derivation.mjs by design, and the PM's follow-up question (is the fold reachable from a subpath already on the eager baseline?) is now answered NO with counter-probed evidence — so the import is the only in-surface spelling and the weight is filed as #5359. File surface honoured: only DraftChangesPanel.tsx, its test, and the changeset.",
      "tests": "All from the repo root at 2efc90d19, heavy runs under flock /tmp/os-heavy-verify.lock. (1) `pnpm exec vitest run packages/app-shell/src/preview/__tests__/DraftChangesPanel.test.tsx` -> 'Test Files 1 passed (1) / Tests 13 passed (13)' (8 pre-existing + 5 new route pins). (2) `pnpm --workspace-concurrency=2 --filter @object-ui/app-shell type-check` -> exit 0 with the script echoed, after `pnpm --filter '@object-ui/app-shell^...' build`; before that build it failed with TS2307 on every workspace dep, so the run was real, not a zero-match pass. (3) eslint on both changed files -> 0 errors, 2 pre-existing warnings. (4) Gates at this sha, all exit 0: check-spec-symbol-derivation, check-control-bytes, check-changeset-presence, check-changeset-no-major, check-phantom-dependencies, check-package-self-import. REVERSE VERIFICATION: reverting the fold turned 4 of the 5 route pins red -- \"expected [ '/api/v1/meta/objects' ] to include '/api/v1/meta/object'\", \"expected [ '/api/v1/meta/capabilities' ] to include '/api/v1/meta/capability'\", \"expected 'objects · 1' to be 'object · 1'\" -- and restoring returned 13/13; the 5th pin is the passthrough control, green in both legs by design. No build artifact sits between the edit and the subject on either leg: the test imports ../DraftChangesPanel by relative path and vitest transforms it from source; the only prebuilt artifact in play is the installed @objectstack/spec dist, which the ablation does not touch. SUBPATH REACHABILITY (PM follow-up, evidence in PR comment 5350102898): canonicalMetaUrlType / META_URL_TO_SINGULAR are exported by ./shared and by NO other entry in the package's 18-subpath exports map -- all 15 code entries grep 0/0 in both index.d.ts and index.mjs, including the /ui and /kernel baseline. Zeros counter-probed on the same files with symbols objectui imports from them today (expandViewContainer 1/5 hits, PageSchema 4, deriveNamespaceFromPackageId 2/2, composeStacks 10), and confirmed by resolving each entry at runtime and filtering its real export list: root 125 exports and /ui 218 exports carry nothing fold-shaped; /shared 71 exports carry the whole family. /kernel and root merely INLINE PLURAL_TO_SINGULAR without exporting it, and it is the wrong map regardless; deriving a fold from /kernel's DEFAULT_METADATA_TYPE_REGISTRY would mean rebuilding restPluralOfMetaType, which objectstack#8424 keeps module-internal on purpose. No deep-path import past the exports map was attempted.",
      "open_questions": [
        {
          "question": "The fix adds a measured +60.1 KB gzipped to every console page load to fold a route segment an admin-only panel emits, and there is no cheaper import. Ship as-is, or pay it down later?",
          "options": [
            "A - Ship as-is (this PR). The only in-surface spelling: a local mirror is refused by the spec-derivation gate, and no eager-baseline subpath exports the fold (measured above). The weight is recorded at the import site and filed as #5359.",
            "B - Follow up on #5359: exclude the spec `shared` subpath from VENDOR_OBJECTSTACK_TEST and switch this fold to `await import()` in the async boundary that already exists, with a build assertion in the spirit of assertLazyLinterStaysLazy. Both halves live in apps/console/vite.config.ts, outside this card's surface. This RELOCATES the bytes off the eager path.",
            "C - Ask upstream for a finer-grained export of the /meta spelling contract. The only option that REMOVES the cost rather than relocating it, and it removes it for every consumer."
          ],
          "recommendation": "A now, C as the better first move if #5359 is ever picked up. The subpath measurement closes the cheap-swap possibility, so nothing further is available inside this card's file surface."
        }
      ],
      "out_of_scope_findings": [
        "filed as #5359: the /meta fold puts @objectstack/spec/shared on the console's eager graph (+213.4 KB min / +60.1 KB gz, measured); the vendor-objectstack chunk group makes lazy loading ineffective and no budget weighs those bytes (related: #5324, #5325, objectui#5266). Updated with the subpath-reachability measurement, which narrows its levers to relocate-vs-remove.",
        "not filed, already tracked: #4940 covers the sibling family in data-objectstack (adapter meta call sites passing a plural type argument through the client). Out of this card's file surface; no duplicate opened. The card's 'zero literal plural routes' premise holds for raw fetch URLs -- #4940's sites are plural TYPE ARGUMENTS handed to the metadata client, so the two statements do not conflict."
      ]
    }

    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions