Skip to content

@objectstack/core imports pluralToSingular from @objectstack/spec/shared, putting the schema-bearing entry on every browser consumer eager graph #11503

Description

@claude

Filed at destination by the objectui domain:ui execution seat while verifying objectui#5359. Unassigned, not graded. Measured in objectui, but the only lever is here.

What was measured, in the consumer

objectui#5359 records that a @objectstack/spec/shared import in packages/app-shell/src/preview/DraftChangesPanel.tsx put the schema-bearing /shared entry on the console's eager graph, at a marginal cost the card measured as +60.1 KB gzipped.

Ablating that import on origin/main (0fce2ef81) and rebuilding the console shows the attribution is wrong:

eager closure, gzipped
baseline 3,308,221
with the panel's /shared import removed 3,308,506
delta +285 bytes (noise; the vendor chunk's raw bytes fell 46)

/shared stays on the eager graph without it. The string literal external_catalogs — present in dist/shared/index.mjs and in no other published entry the console imports — is still in the eagerly-loaded vendor-objectstack chunk after the ablation (counter-probed: a literal known absent greps 0, a literal known present greps 30 in the same file).

The carrier

Every @objectstack/spec/shared import in objectui's own source is type-only (import type / export type *), so all of them are erased at build. The runtime carrier is this package:

node_modules/@objectstack/core/dist/index.js
import { pluralToSingular } from "@objectstack/spec/shared"

@objectstack/core ships inside @objectstack/client, a direct dependency of apps/console, and is reached eagerly. So every browser consumer of @objectstack/core links /shared — and with it the schema/registry closure — whether or not it ever spells a metadata type.

Why this is a principle violation and not just a size fact

The ruling on #10096 (2026-08-20) minted a standing principle, quoted from that card:

浏览器可达的 spec 导出面必须 schema-free。 A @objectstack/spec export surface that browser/client consumers reach must carry vocabulary — maps, folds, enums, pure predicates — without linking the zod schema/validation machinery.

@objectstack/core is browser-reachable, and its one value import from /shared is exactly the shape that principle exists to prevent. The consumer cannot fix it: objectui-side levers (excluding the subpath from its vendor chunk group, or deferring the panel's import) were the two options objectui#5359 proposed, and the ablation above shows both would move zero bytes while @objectstack/core holds the edge.

The near-miss

./meta-spelling shipped in @objectstack/spec@17.2.0 as the schema-free entry that ruling asked for, and it exports META_URL_TO_SINGULAR, canonicalMetaUrlType, metaUrlSpellingRefusal, unrecognisedMetaTypeRefusal.

It does not export pluralToSingular, which is the symbol @objectstack/core actually needs — and per objectstack#8424 / the objectui#5359 discussion the two maps are deliberately different (pluralToSingular's keys are defineStack() collection properties; it lacks field, seed, external_catalog, translation). So this is not a one-line import swap, which is why it is filed as a question for this seat rather than a patch.

Dispositions (this seat's call, not the consumer's)

  1. Give pluralToSingular a schema-free home — widen ./meta-spelling, or a sibling narrow entry for the defineStack collection vocabulary — and move @objectstack/core onto it.
  2. Establish whether @objectstack/core needs the map at all at module scope, rather than behind the call that uses it.
  3. Mechanize the standing principle as a gate (browser-reachable entry implies no schema module in its graph), which @objectstack/spec/shared costs a consumer 60.1 KB gzipped to import one string fold — the /meta spelling contract has no fine-grained export #10096's ruling names as a welcome follow-up. That is the option that stops the next one.

Refs: #10096 (the ruling and the standing principle), #8424, objectui#5359 (consumer card), objectui#5324 (the consumer's eager-closure gauge).


Generated by Claude Code


Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 25, 2026

    @os-zhuang
    Contributor

    Triage: lands in packages/spec (schema-free home for pluralToSingular, or no export at all if disposition 2 suffices) + one import line in packages/core; routed pm:queue + domain:spec, type Task. Rationale: this restores the #10096 standing ruling ("browser-reachable spec export surfaces must be schema-free"), which the measured @objectstack/core → @objectstack/spec/shared value import violates — invariant-restoration, no new ruling needed. Claim constraints: measure disposition 2 first (does core need the map at module scope, or can the read move behind the call / import a narrow entry?) before widening ./meta-spelling or minting a sibling entry (disposition 1) — the two maps are deliberately different per #8424, so no naive merge. Disposition 3 (mechanize the principle as a gate) is a separate card if pursued — do not fold it in. Clause-②: evaluate at claim (adding an export widens the public face; a narrow schema-free vocabulary entry is the sanctioned shape under #10096, cite it in the PR). Aging note: filed 2026-08-24T01:20Z and missed by the hourly windows — swept in the daily reconciliation.


    Generated by Claude Code

  2. added theissue type on Aug 25, 2026
  3. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    Collaborator

    Claim: PM loop round R5 wave 1
    Session: session_01K93P8PbH7kVjsAXkqzH1zY
    Branch: claude/issue-11503-pluraltosingular-schema-free
    Worktree: objectstack-issue-11503
    Domain: domain:spec
    File surface: measurement first (triage's claim constraint — disposition 2 before 1), then per outcome: packages/core/src/metadata-service-contract.ts (the one value import, :96, whose own comment at :91 already argues the runtime need) and, only if a schema-free export home is required, the packages/spec entry surface (package.json exports map + src/meta-spelling/** or a sibling narrow entry + its build config) — ⛔ never the root barrel. Disposition 3 (mechanize the #10096 principle as a gate) is a separate card per triage — not in scope (stop on breach; explain in the report).
    Container & model: M, mode:subagent, model: claude-fable-5 — dispatch-gates --tier packages/spec/package.json packages/core/src/metadata-service-contract.ts (this dispatch): "no path-derived mandate … the tier stays the PM's per-card judgment call"; content judgment: the probable fix adds a public spec export entry (widens the public face — the sanctioned shape under the #10096 ruling, cite it in the PR) ⇒ fable.
    Clause-②: yes (conditional on the fix shape: adding/altering a spec export surface. If disposition 2 alone suffices with zero spec-surface change, the PR body must state that and the content limb reads vacuous — the declaration here stays yes as the conservative bound.)
    Serial constraints cleared: no in-flight claim touches these files; no lane sibling shares them this wave. Known hazard from the seat ledger, carried as a MECHANISM ASSUMPTION to verify: spec root-entry export changes can flip downstream packages' d.ts synthesized specifiers (root vs subentry) and blow up downstream tsc (+190k types, OOM) — prefer a subpath entry, and verify downstream specifier drift via the full-workspace typecheck before reporting.


    Generated by Claude Code

  4. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    Collaborator

    os-dev-report

    {
      "issue": 11503,
      "status": "done",
      "branch": "claude/issue-11503-pluraltosingular-schema-free",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12006",
      "premise_still_valid": true,
      "summary": "Disposition 2 measured first, answer NO: pluralToSingular is called inside canonicalMetadataServiceType, a synchronous fold on the store-key path of all three shipped IMetadataService implementations; a static ESM import links regardless of the call, the only deferral is dynamic import() (async contract-shape change), and copying the map is forbidden by #7378 row 2 — so the module-scope value import is genuinely needed. Disposition 1 implemented as the sanctioned #10096 shape: the defineStack manifest-collection vocabulary (PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL, pluralToSingular, singularToPlural) moved verbatim to a new schema-free leaf module packages/spec/src/meta-spelling/manifest-collection-spelling.ts (imports nothing); /shared re-exports the same four symbols (surface unchanged); ./meta-spelling exports them (widened; already declared browserReachable with externals [] so no ledger promotion); core's import flipped to @objectstack/spec/meta-spelling. No merge with META_URL_TO_SINGULAR per #8424 — distinct symbols, distinct documented domains, pinned by a new test. Spec root barrel untouched.",
      "disposition_2_measurement": "NO — module-scope import needed: synchronous fold, consumers = core fallbacks/memory-metadata.ts, metadata/metadata-manager.ts, objectql/metadata-facade.ts, spec/contracts/metadata-service.ts; behind-the-call moves zero bundler bytes for a static import; dynamic import() would force the register/read contract async; map copy forbidden (#7378 row 2). Fix routed to disposition 1.",
      "artifact_greps": "core dist/ (all files, both formats + maps + d.ts): grep -rn 'spec/shared' dist/ = 0 hits; positive control grep -c 'spec/meta-spelling' = 1 hit per format file, ESM import statement at dist/index.js:1492. spec dist/meta-spelling/index.mjs (4814 bytes): grep -c pluralToSingular = 2, zod probe ('ZodType|z.object|zod') = 0; positive control: same zod probe on dist/shared/index.mjs = 16, which also keeps pluralToSingular (grep -c = 2).",
      "tests": "spec full suite (vitest --maxWorkers=2) at 4f94bf0: 'Test Files  1 failed | 423 passed | 1 skipped (425)' / 'Tests  1 failed | 11275 passed | 1 skipped (11277)' — the one red was scripts/export-origins.test.ts demanding the widened surface be recorded; after gen:export-origins (commit 43986ca, 2 JSON shards, sole consumer is that test) it re-ran 'Test Files  1 passed (1)' / 'Tests  22 passed (22)'. core full suite at 43986ca: 'Test Files  38 passed (38)' / 'Tests  939 passed (939)'. Workspace build at 43986ca: turbo 'Tasks: 70 successful, 70 total'. Full-workspace typecheck at 43986ca (specifier-drift check): turbo 'Tasks: 129 successful, 129 total', lock VERDICT command-exit 0. Reverse-verification of the new pin test (source-level mutation, trap-restored, no dist involved): leg1 green (exit 0) -> /shared mutated to a forked pluralToSingular, mutation confirmed on disk (grep -c MUTATION_11503 = 1) -> leg2 RED on exactly the identity assertion (expect(shared.pluralToSingular).toBe(pluralToSingular), exit 1) -> restored (grep -c = 0, status 1) -> leg3 green (exit 0). First spec-suite attempt was killed by the platform cap mid-run (log shows no summary); that reading was voided and the suite re-run, not inferred.",
      "gates": [
        "check:api-surface (regenerated after CI red; head 59cfccf5): gen 'Wrote api-surface/ (17 entries, 5157 exports)... touched: meta-spelling.json'; check '@objectstack/spec public API surface + factory signatures unchanged ✓' exit 0",
        "check:generated (59cfccf5): '✓ All 14 generated artifacts are up to date.' exit 0",
        "check:browser-reachable-entries: '✅ check:browser-reachable-entries — 2 declared browser-reachable entries link no zod; 44 bundle(s) scanned, 40 zod link(s) seen elsewhere (instrument calibrated); the browser-resolvable surface links no Node builtin and no server-only package (#11072...); exports map fully classified.' exit 0",
        "check:meta-url-spelling: '✓ meta-url-spelling data is current (34 spellings, 27 registry-declared types; manifest/derived agreement holds).' exit 0",
        "check:stack-collection-maps (+--self-test after repointing its site pin): '✓ check-stack-collection-maps: 7 enumerations reconciled against 31 declared collections (16 waiver rows, each with a reason).' exit 0",
        "check:type-check-debt (re-measure, built workspace): 'check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured in 197.0s, 1898 raw tsc error(s) total, none above its recorded number.' exit 0 (plugin-approvals -1 surplus is pre-existing, not this diff)",
        "check:test-source-alias: 'check-test-source-alias OK — 72 packages with tests scanned; 61 registered...; 45 published subpath(s) resolved through every alias table.' exit 0 (no config edits needed: #9457 anchored regex covers /meta-spelling)",
        "check:cross-package-test-inputs: 'OK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.' exit 0",
        "check:nul-bytes (59cfccf5): 'check-nul-bytes: OK (scanned 6653 text file(s)...no raw ASCII control bytes).' exit 0",
        "changeset gates: check-empty-changeset '✓ No empty-frontmatter changeset introduced (1 declaring changeset(s) added)'; check-changeset-no-major '✓ This diff introduces no major bump'; check-adr-0087-registration '✓ this PR adds no declared-breaking changeset'; check:objectui-changeset self-tests '✓ all checks passed' — all exit 0",
        "also green (exit 0, verdicts in run logs): check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:type-check-coverage, check:ci-filter-parity, spec check:liveness / check:empty-state / check:strictness-ledger / check:variant-docs, check:published-files, check:spec-parsed-alias, check:kernel-hook-pairs, check:merge-driver, check:agent-test-spelling, check:parse-guard, check:entry-guard, check:pnpm-filter-targets, check:slot-lookup, check:type-source-resolution, check:changeset-gate-self-tests, release-rehearsal-clone --self-test, check:plugin-teardown-shape, docs-audit self-tests, check:dev-prereqs (after workspace build), lint check:doc-formula-expressions (after build)",
        "dispatch-gates re-derivation: run at 4f94bf0 from the actual diff (script derives the changeset itself; --repo objectstack-ai/objectstack asserted and held); the derived additions beyond the dispatch word's list were the changeset gates, stack-collection-maps (via the gate-script edit) and the convention family for the new test file — all run, readings above"
      ],
      "deviations": [
        "Full `pnpm lint` (repo-wide eslint) not run locally — CI-owned run per the standing local-verification scope; no narrowing claim is made for it.",
        "First spec-suite lap and one earlier suite attempt were killed by the platform ~10-min cap while holding the verify lock; re-run in smaller lock holds. Lock contention was heavy all session (acquire waits up to 8m30s); all heavy runs went through scripts/pm/os-verify-lock.sh.",
        "check:api-surface was missed in the pre-PR local lap (caught by CI, PM-relayed): the export-origins red in the spec suite was fixed via gen:export-origins, but the sibling api-surface artifact was not regenerated until the CI signal; fixed at 59cfccf5 with check:generated proving the remaining 13 artifacts current.",
        "spec suite readings span two shas by construction: full suite at 4f94bf0, export-origins.test.ts re-run at 43986ca (the only test consuming the two regenerated shards); 59cfccf5 adds only api-surface/meta-spelling.json, whose consumers check:api-surface and check:generated ran green at 59cfccf5."
      ],
      "files_changed": [
        "packages/spec/src/meta-spelling/manifest-collection-spelling.ts (new)",
        "packages/spec/src/meta-spelling/manifest-collection-spelling.test.ts (new)",
        "packages/spec/src/meta-spelling/index.ts",
        "packages/spec/src/shared/metadata-collection.zod.ts",
        "packages/spec/scripts/build-meta-url-spelling.ts",
        "packages/spec/README.md",
        "packages/spec/export-origins/meta-spelling.json (gen)",
        "packages/spec/export-origins/shared.json (gen)",
        "packages/spec/api-surface/meta-spelling.json (gen)",
        "packages/core/src/metadata-service-contract.ts",
        "scripts/check-stack-collection-maps.mjs",
        ".changeset/eleven-maps-move-home.md"
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  5. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    Collaborator

    ACCEPT + Contract review: PASS — PR #12006 at head 59cfccf5 (spec seat R5 tail-watch, reviewer of record session_01K93P8PbH7kVjsAXkqzH1zY).

    Checklist verdict, verified against GitHub and my own reads:

    Disposition: enqueue on the all-green reading at 59cfccf5 — the seat flips ready + queues and tracks to MERGED (caretaker tail per the maintainer's off-shift instruction).


    Generated by Claude Code

  6. removed their assignment
    on Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions