Skip to content

objectql: an object extension's display patch is carried and applied by mergeObjectDefinitions; a patch naming a missing field or option is refused at install and validate #28565

Closed

Description

@objectstack-fleet

epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848

Filing gate: ④ child of epic #28236 (P2, cut 2026-11-01 on the maintainer's 「四项提速措施全部按推荐执行」, schedule reviewed) · plan of record: ADR-0146 Rollout P2 "N-23a/b/c object extension — the field-patch whitelist". Split from the review's one-card N-23b: the __c doors stay #28564. ⛔ Not a claim.

Blocked-by: #28560

Part of #28236 (P2, child N-23d) · governs: ADR-0146 D10 second rule ("mergeObjectDefinitions applies it after the additive merges, and the ADR-0029 D9.2a scalar yield applies to it") · lane: domain:server (packages/objectql, packages/metadata-protocol read fold) · size: S–M.

Business reason. The patch N-23a declares has to reach the screen. "Industry" reads "Sector" on every form, list and API read of HotCRM's account, and the vendor's field otherwise stays as shipped. A patch aimed at a field that does not exist must be named, never silently ignored.

Measured (35b178bd0a).

  • objectql/src/engine.ts:8815-8833: the manifest loop builds extDef from exactly fields, label, pluralLabel, description, validations and indexes, then calls registerObject(…, 'extend', priority). It is the only 'extend' registration in objectql, metadata-protocol and runtime (git grep).
  • objectql/src/registry.ts:559-601#mergeObjectDefinitions: fields are spread, validations merged by name, indexes concatenated. The three scalars yield when tenantAuthored holds them (ADR-0029 D9.2a). The fold calls it at about :4140-4170. Removing an extender recomputes through subtractExtenderContributions (:5332).
  • metadata-protocol/src/protocol-object-extender-fold.ts: the by-name read folds extenders onto a MetadataService body.
  • Registration refusals precedent: ExtensionMembershipKeysRefusedError / ExtensionDefaultRowScopeRefusedError in registerObject (about :3826-3836), before the contributor list opens.
  • content/docs/data-modeling/object-extensions.mdx:65 documents fields replacing a same-named field.

Wanted.

  1. Carry. extDef carries N-23a's slot. The registry fold and the by-name read fold serve it, so the list, the by-name read and the layered read agree.
  2. Apply. mergeObjectDefinitions applies each patch after the additive merges:
    • whitelisted keys onto the base field;
    • option labels onto the option whose stored value matches.
      The patch yields where D9.2a makes the three scalars yield. Two extensions that patch one key: the higher priority wins, as for the scalars.
  3. Target check. A patch naming a field the target does not have, or an option value the field does not have, is refused:
    • at both install doors (the installed base is in the registry: ADR-0141 D2 admitted the dependency first);
    • at os validate when the target is in the composed stack.
      At the boot fold the same patch is not applied and is named once in the log, ⛔ never a boot refusal (AGENTS.md Startup registry reads).
  4. Who may patch is not narrowed here: any extension may patch display properties. .custom extension fields without __c are metadata-protocol + cli + install-local: __c extension fields only in a .custom package — refused at save/publish/restore, os validate, both install doors and duplicate #28564's refusal.
  5. The slot's [EXPERIMENTAL — not enforced] marker and any planned liveness row flip to live, citing mergeObjectDefinitions. Metadata-protocol: semantic (a declared key gains its effect), minor changeset. object-extensions.mdx gains the patch beside fields.

Tests.

  • A patch changes the label, description, help text, placeholder and one option label on all object reads; type, required and the option values are unchanged.
  • Uninstalling the extender restores the vendor's text.
  • The D9.2a yield: a tenant-authored label is kept.
  • Priority order between two patching extensions.
  • An unknown field or option value is refused at install and validate, and named at boot without failing it.
  • The existing extension fold tests stay green.

Release safety. Display-only and additive. Nothing stored changes, and an extension without a patch folds as before. N-23a in Blocked-by: supplies the shape.

Not here. The slot (#28560); __c at every door (#28564); Studio add-field (#28575). The runtime write door for an object_extension item into a writable package (#28574): no such door exists today (objectql/src/plugin.ts:178-182, the manifest loop is the only 'extend' registration). That door hands the registry the whole parsed body, so the carry of item 1 applies to it without a second list.

Dedupe. Searched mergeObjectDefinitions is:open (2 hits, both seat cards) and field patch whitelist extension (on topic: #28236, #28467 only). No card applies the patch.

Links back: #28236, #28467, ADR-0146 D10, ADR-0029 D9.2a, ADR-0141 D2.


Generated by Claude Code

Activity

  1. added
    enhancementNew feature or request
    priority:p1High: required for production / M2
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 1, 2026
  2. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848
    2026-11-01T12:41Z

    Unblocked · p1 · pm:queue (epic stall patrol)

    ⛔ Not a claim.

  3. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

    Claim: PM loop round 1 · card thread · 2026-11-01T13:20Z
    Session: session_01FEkQ5oc56xChD6Wrc4cGSt (the seat); the card thread is session_01FEkQ5nSMgkteiemkoN31vf
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-nye4b2 (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#5
    Ruling-ref: 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z) with the epic's p1 grade as filed; the epic stall patrol's unblock record (2026-11-01T12:41Z, Blocked-by: #28560 closed via PR #28650). Split agreed with seat 1, which holds #28564 (claim 6558411066).
    File surface: packages/objectql/src/engine.ts (only the manifest loop's extDef build before the 'extend' registration), packages/objectql/src/registry.ts (mergeObjectDefinitions, its fold, subtractExtenderContributions, a boot-fold log line), packages/metadata-protocol/src/protocol-object-extender-fold.ts; one new target-check judge module plus one call line each at metadata-protocol/src/protocol-package-install.ts#installPackage, cloud-connection/src/marketplace-install-local-plugin.ts#handleInstall (early, beside N-09's judge point) and packages/cli/src/commands/validate.ts; the N-23a slot's EXPERIMENTAL marker / liveness row; content/docs/data-modeling/object-extensions.mdx; tests; changesets. ⛔ No protocol.ts doors, duplicatePackage, override-declarations.ts or __c judge (#28564).
    Container & model: M, mode:thread, model and effort as the thread was opened
    Clause-②: no
    Metadata-protocol: semantic
    Responsibility: n/a — not a defect card
    Thread-read: 6558078355
    Premise: holds (read at origin/main 2678600d: N-23a's per-field patch slot is on main via PR #28650; the manifest loop's extDef carries only fields, label, pluralLabel, description, validations and indexes)
    Serial constraints cleared: #28564 (seat 1) adds its own judge and call lines at the same three doors; agreed in parallel: separate files, separate call lines, the #28565 dev writes door calls last and merges origin/main before the PR, the second to land resolves. #28604 (seat 2) works in handleInstall's rehydrate and purge/reseed reads, not the early checks. No other in-flight claim names objectql/src/registry.ts or the extender fold.
    Resume-from: none — fresh from origin/main 2678600d

    State: pm:queue → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28565.md in the project.

  4. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-project-manager · session_01FEkQ5nSMgkteiemkoN31vf
    os-dev-report
    {
    "issue": 28565,
    "status": "done",
    "branch": "claude/project-thread-nye4b2",
    "pr": "#28728",
    "session": "session_01FEkQ5nSMgkteiemkoN31vf",
    "premise_still_valid": true,
    "summary": "An object extension's fieldPatches is now applied: the engine's extDef carries the slot, and mergeObjectDefinitions writes the BASE_FIELD_DISPLAY_WHITELIST keys onto the named field and option labels by stored value, after the additive merges, yielding to tenant-authored text (D9.2a, tenantPatchedDisplayKeys) with higher priority winning; list, by-name and layered reads agree through the one fold. One judge in @objectstack/metadata-core (fieldPatchTargetMisses / fieldPatchTargetRefusals) refuses a patch naming a missing field or option (and an option patch on a picklist-bound field) at install-local step 2e (400 VALIDATION_ERROR) and os validate step 3a-iv; the boot fold does not apply it and warns once. Marker flipped to live citing mergeObjectDefinitions, docs page gains the patch, minor changeset with Metadata-protocol: semantic.",
    "tests": "Head 043407e. objectql registry-extension-field-patches.test.ts 9/9; with registry.ts + engine.ts at base 2678600: 8 red (ninth = no-patch control); engine.ts alone at base: 1 red (registerApp carry); reportFieldPatchMisses call removed: 1 red. cloud-connection marketplace-install-local-field-patch-targets.test.ts 5/5; step 2e call removed: 4 red (all negative pins), lit control green. cli test/validate-field-patch-targets.test.ts 2/2 (spawn); step 3a-iv call removed: 1 red. metadata-core judge 10/10 and cli utils walk 5/5 (new modules). spec field-customization.test.ts: marker pin 1 red with object.zod.ts at base, 67/67 at head. Each ablation restored with blob equal to HEAD. Full suites after a whole-repo pnpm build on the merged tree: objectql 515 files / 9915 tests, metadata-core 22 / 885, cloud-connection 66 / 801, spec src/data 154 / 4751 (1 todo), cli unit 365 / 5172, all exit 0; tsc --noEmit objectql, cloud-connection, cli exit 0; eslint on the 15 changed TS files exit 0. After the final main merge the new test files re-ran green; one run of the cli trio exited 1 while other suites ran beside it, three solo re-runs exited 0, log not kept.",
    "gates": "dispatch-gates --commands re-derived at head: identical 114. --ran: 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. First pass had 5 non-zero, each traced to the checkout and re-run green: test cli unit exit 1 (setup-integrations-order.pin read a pre-merge service-automation dist; rebuilt, green); check-plugin-teardown-shape --self-test exit 3 (shallow clone; fetched pinned commit 621a487, 48 cases pass); check:skill-examples, check:dual-build-cjs-loads, check:i18n-coverage exit 3 (no built output; after pnpm build exit 0 each). spec check:generated exit 0 on the merged tree. CI check-runs on PR 28728: not yet concluded at report time.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "0",
    "api_writes": "4 — git push x2 to claude/project-thread-nye4b2 (e1583eb, 043407e); relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches, run 41322056802, PR 28728, body read back 12303/12303 bytes identical, assignees none); relay comment (this report, post-stamped.mjs)",
    "deviations": [
    "installPackage call line NOT written: protocol-package-install.ts parses PackageInstallManifestSchema (strict ManifestSchema), which refuses objectExtensions as an unknown key before anything runs (safeParse false, "Unrecognized key(s) on this package manifest: objectExtensions"), so a call there can never fire or be ablated. The only install door that delivers an extension today is install-local (incl. os package install FILE).",
    "packages/cli/test/validate-build-gate-parity.test.ts edited (outside the named file surface): its closed roster (landed via main) requires the new validate gate to be classified; added to VALIDATE_ONLY_GATES with its reason.",
    "ObjectExtensionSchema.guidance.actions string left unchanged under ruling 3 (only the slot marker moves in packages/spec); it still lists the merged keys without fieldPatches.",
    "protocol-object-extender-fold.ts needed no edit: the by-name read already calls registry.foldObjectExtendersOnto, the one apply path (measured)."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "class: b · reach: os build emits an artifact whose fieldPatches names a missing field; install-local then refuses it · evidence: compile.ts runs no target check; recorded as the VALIDATE_ONLY_GATES row · dedupe words: os build field patch target check · carrier: PR 28728 body · noted, not filed",
    "class: c · reach: skills/objectstack-data/SKILL.md section Object Extension Model names fields/priority only · evidence: Skills-docs line in the PR body · dedupe words: skills object extension fieldPatches · carrier: PR 28728 body · noted, not filed"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

    ACCEPT → PR #28728 comment 6560471454 · head f82fd682 · 2026-11-01T16:11Z. Contract review PASS (comment 6560336731); should-fix findings filed as #28733, skills follow-up #28730. Landing: pr_ready + automerge_enable through the relay; the merge queue closes this card.

  6. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

    Landed · 2026-11-01T18:14Z · PR #28728 merged through the merge queue as e62dcc567b; this card closed as completed by its Fixes line.

    reading value
    PR state merged, e62dcc567b
    card closed (completed); pm:dispatched removed and the seat unassigned in this stroke, domain:* / area:* kept

    Unblocked in the same pass: #28730 (skills follow-up) and #28733 (the contract reviews' should-fix findings), pm:blocked → pm:queue.

  7. added a commit that references this issue on Nov 1, 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

    Labels

    area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions