Skip to content

feat(objectql): an object extension's fieldPatches relabels the vendor field on every read, refused at install and os validate when it names nothing (#28565) - #28728

Merged
objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/project-thread-nye4b2
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/project-thread-nye4b2

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

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

Requested by Leehom · project thread

Fixes #28565

Clause-②: no
Metadata-protocol: semantic
Skills-docs: follow-up #28730

维护者速读

  • 改了什么:对象扩展的「字段显示补丁」(objectExtensions[].fieldPatches,上一版只声明未生效)现在真的生效了:引擎注册扩展时带上它,注册表合并(mergeObjectDefinitions)在字段合并之后把白名单里的 label、description、帮助文字、占位符和选项 label 写到厂商字段上;字段类型、必填、选项取值不变。补丁指向目标对象没有的字段或选项值时,在市场安装口和 os validate 拒绝;启动时只记一次日志、不拦启动。
  • 为什么改:HotCRM 的 "Industry" 要在每个表单、列表和 API 读取里显示成 "Sector",而字段本身仍归厂商,厂商升级照样进来。指向不存在字段的补丁以前会悄悄什么都不做。
  • 风险与回滚:只影响写了 fieldPatches 的扩展;没写的扩展合并结果逐字节不变。租户自己改过的文字照旧优先(ADR-0029 D9.2a),两个扩展冲突时 priority 高者胜。回滚 = revert 本 PR。
  • 先看:① packages/objectql/src/registry.ts 的 patchedField / applyFieldPatches 与 tenantPatchedDisplayKeys(合并与让位);② packages/metadata-core/src/extension-field-patch-targets.ts(三处共用的目标检查);③ packages/cloud-connection/src/marketplace-install-local-plugin.ts 新增的 2e 步。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/registry-extension-field-patches.test.ts,预期 9 个测试全绿。
  • 需要你做的:无,席位落地。

What changes

  • Carry — packages/objectql/src/engine.ts: the manifest loop's extDef carries fieldPatches beside fields, so the 'extend' registration hands the slot to the registry. Re-measured at 2678600d: still the only 'extend' registration in objectql, metadata-protocol and runtime (git grep "'extend'", one call site, engine.ts:8996).
  • Apply — packages/objectql/src/registry.ts:
    • mergeObjectDefinitions gains one step after the scalar overrides: applyFieldPatches writes each BASE_FIELD_DISPLAY_WHITELIST.field key onto the named field and each BASE_FIELD_DISPLAY_WHITELIST.option key onto the option whose stored value the patch names. Keys are read from the constant, so a widened whitelist widens the merge. The type of the slot is the spec's own (ObjectExtensionParsed['fieldPatches']); there is no second patch shape.
    • D9.2a yield: tenantPatchedDisplayKeys computes, once over the fold's base, which patched field/option keys the tenant authored away from the packaged owner's value, with the same two "no opinion" edges as tenantAuthoredScalars (no owner; base IS the owner by reference). The comparison is scalarOverridesPackagedBase itself, called through a one-line adapter because its exported key union names only the object scalars and message.
    • Priority: the fold already applies contributors in ascending priority, so the higher one writes last, exactly as for label.
    • Boot: reportFieldPatchMisses names, once per object/package/field/option, a patch the fold could not apply, at warn, worded as the observed state. Never a refusal: the fold runs on every read while a boot is still registering (AGENTS.md Startup registry reads), and the patch applies on the next read once the field arrives.
    • subtractExtenderContributions is untouched: patches are idempotent over an already-folded base (the base carries the patched text, reads as authored, and the patch yields to the same value), the way the scalars are. Pinned by the "idempotent over a folded body" assertion.
  • By-name and layered reads — no edit. protocol-object-extender-fold.ts already calls registry.foldObjectExtendersOnto, which runs foldExtendersOntoDefinition → mergeObjectDefinitions, the one apply implementation (measured; the order's assumption holds). The code layer (getArtifactItem('object') → resolveOwnerLayer) goes through the same fold.
  • Target check — packages/metadata-core/src/extension-field-patch-targets.ts (new): fieldPatchTargetMisses (one extension against one target), fieldPatchTargetRefusals (a manifest's objectExtensions, located) and fieldPatchTargetRefusalMessage. Misses: a field the target lacks, an option value its field lacks, and an option patch on a picklist-bound field (its options are written over the field after the fold, so an option patch there could never be served). metadata-core is the one package all three readers already depend on. One call line each:
    • packages/cloud-connection/src/marketplace-install-local-plugin.ts#handleInstall, new step 2e, right after N-09's override write gate (2d) and before the hot-register (3): 400 VALIDATION_ERROR, every refusal in error.details.refusals, nothing registered, written or synced. Targets: one the same artifact delivers, else the registry's resolved object.
    • packages/cli/src/commands/validate.ts, new step 3a-iv after the override gate, over packages/cli/src/utils/field-patch-targets.ts (the walk: objects across the composed stack and every packages[] entry).
    • the registry fold's boot log (above).
  • Marker flip — packages/spec/src/data/object.zod.ts: fieldPatches' .describe() drops [EXPERIMENTAL — not enforced] and cites mergeObjectDefinitions; its TSDoc, the @example comment and the guidance code comment follow. No liveness ledger row exists for the slot (grep -l ObjectExtension packages/spec/liveness/*.json: none). content/docs/references/data/object.mdx regenerated (gen:docs, the one artifact check:generated named stale). The spec test that pinned the marker now pins its absence.
  • Docs — content/docs/data-modeling/object-extensions.mdx: fieldPatches in the contribution table, a "Relabelling a vendor field" section, and the D9.2a callout extended.
  • packages/cli/test/validate-build-gate-parity.test.ts: the new validate gate is classified in VALIDATE_ONLY_GATES with its reason (the closed roster requires it); the build-side gap is reported below.
  • .changeset/28565-field-patch-applied.md: minor on objectql, metadata-core, cloud-connection, cli and spec (the kind is semantic; each package's files[] ships dist, which carries the change after a build).

Tests

All readings at head 043407e6 unless named. Red-on-base means the change's source files restored to 2678600d with the new tests kept.

Test file Head Base / ablation
packages/objectql/src/registry-extension-field-patches.test.ts (9) 9/9 registry.ts + engine.ts at base: 8 red (the ninth is the no-patch control). engine.ts alone at base (carry ablation): 1 red, the ObjectQL.registerApp carry test. reportFieldPatchMisses call removed: 1 red, the warned-once test.
packages/metadata-core/src/extension-field-patch-targets.test.ts (10) 10/10 new module, absent on base
packages/cloud-connection/src/marketplace-install-local-field-patch-targets.test.ts (5) 5/5 step 2e call line removed: 4 red (every negative pin); the lit control stays green
packages/cli/src/utils/field-patch-targets.test.ts (5) 5/5 new module, absent on base
packages/cli/test/validate-field-patch-targets.test.ts (2, spawns the CLI) 2/2 step 3a-iv call line removed: 1 red (the negative pin); the control stays green
packages/spec/src/data/field-customization.test.ts green the marker assertion reds on base (it pins the EXPERIMENTAL marker's absence)

Each ablation was restored and the file's blob checked equal to HEAD before the next one.

Existing extension fold tests stay green: the full suites after the last two main merges (whole-repo pnpm build, exit 0, then):

Suite Result
@objectstack/objectql 515 files, 9915 tests, exit 0
@objectstack/metadata-core 22 files, 885 tests, exit 0
@objectstack/cloud-connection 66 files, 801 tests, exit 0
@objectstack/spec src/data 154 files, 4751 tests (1 todo), exit 0
@objectstack/cli unit tier 365 files, 5172 tests, exit 0
tsc --noEmit objectql, cloud-connection, cli exit 0 each
eslint over the 15 changed TS files exit 0

After the final merge (043407e6, no file overlap with this diff) the five new test files were re-run: all green. One run of the CLI trio during that step exited 1 while other suites ran beside it; three re-runs alone exited 0, and its log was not kept, so it is reported here rather than explained.

Derived gates (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived at head: identical list). --ran verdict: 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. First pass: 109 exit 0 and five that did not. Each was traced to the checkout, not the diff, and re-run green:

Acceptance notes

  • The protocol install primitive is not a door a patch can reach. The order rules a call line at protocol-package-install.ts#installPackage; measured, its manifest is parsed by PackageInstallManifestSchema (strict ManifestSchema), which refuses objectExtensions as an unknown key before anything else runs: PackageInstallManifestSchema.safeParse({ id: 'com.acme.ext', name: 'E', type: 'app', version: '1.0.0', objectExtensions: [...] }) → false, "Unrecognized key(s) on this package manifest: objectExtensions" (spec dist built from 2678600d). A call line there could never fire and could not be ablated, so none was written. The only install door that delivers an extension today is POST /api/v1/marketplace/install-local (including os package install <file>, which posts there); the card's "Not here" names the future runtime write door for an object_extension item (metadata-protocol + objectql: an object extension is an authorable item of a writable package — saved and published through the existing doors, registered as 'extend', exported as objectExtensions #28574).
  • os build does not run the target check. It still emits an artifact the install door then refuses; recorded as the VALIDATE_ONLY_GATES row. Wiring it into compile.ts is outside the card's three doors.
  • Option patches on picklist-bound fields are refused, not applied: the served options of such a field are the picklist's, written after the fold (finishServedObject), so an option label patched in the fold would be overwritten on every read.
  • ObjectExtensionSchema.guidance.actions still reads "the merge carries fields, label, pluralLabel, description, validations and indexes only". It is a refusal sentence about actions, so it is left as is under ruling 3 (only the slot's marker moves in packages/spec); it now omits fieldPatches from that list.
  • Declaration lines are the claim's, copied. The install and validate refusals are new refusals of a key that was declared [EXPERIMENTAL — not enforced]; the ruling records the kind as semantic.
  • 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 (the __c judge at the same doors) has not landed at the time of this PR; whichever lands second sits its call line beside the other's.

Verification

pnpm --filter @objectstack/objectql exec vitest run src/registry-extension-field-patches.test.ts — 9/9 at head 043407e6; 8 red with registry.ts and engine.ts restored to 2678600d (the ninth is the no-patch control).


Generated by Claude Code

claude added 12 commits November 1, 2026 13:28
…ect read; the field-patch target judge

Claude-Session: https://claude.ai/code/session_01FEkQ5nSMgkteiemkoN31vf
Co-authored-by: Claude <noreply@anthropic.com>
… in mergeObjectDefinitions

ADR-0146 D10 second rule: whitelisted display keys and option labels by stored
value, after the additive merges; ADR-0029 D9.2a yield; priority order; a patch
naming a missing target is skipped and named once at the fold.

Claude-Session: https://claude.ai/code/session_01FEkQ5nSMgkteiemkoN31vf
Co-authored-by: Claude <noreply@anthropic.com>
…ing mergeObjectDefinitions

Claude-Session: https://claude.ai/code/session_01FEkQ5nSMgkteiemkoN31vf
Co-authored-by: Claude <noreply@anthropic.com>
…field or option its target lacks (step 2e)

Claude-Session: https://claude.ai/code/session_01FEkQ5nSMgkteiemkoN31vf
Co-authored-by: Claude <noreply@anthropic.com>
… its in-stack target lacks; changeset

Claude-Session: https://claude.ai/code/session_01FEkQ5nSMgkteiemkoN31vf
Co-authored-by: Claude <noreply@anthropic.com>
…nerated object reference

Claude-Session: https://claude.ai/code/session_01FEkQ5nSMgkteiemkoN31vf
Co-authored-by: Claude <noreply@anthropic.com>
…the build/validate parity roster

Claude-Session: https://claude.ai/code/session_01FEkQ5nSMgkteiemkoN31vf
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:data tests tooling labels Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/cloud-connection, @objectstack/metadata-core, @objectstack/objectql, @objectstack/spec, touching 33 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/metadata-core/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 272fec05bdf48e8dd06f2be252ef4aeb67740f41.

⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/metadata-core/src/index.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 82 pages)
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 180 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 272fec05bdf48e8dd06f2be252ef4aeb67740f41 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e7b32dedf15cbda1815aacc8c293620e2cae86e1 — the merge of head d4414410c797989ecdf4983e7633b32df94619d3 into base 272fec05bdf48e8dd06f2be252ef4aeb67740f41, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e7b32dedf15cbda1815aacc8c293620e2cae86e1 && git checkout e7b32dedf15cbda1815aacc8c293620e2cae86e1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 272fec05bdf48e8dd06f2be252ef4aeb67740f41 d4414410c797989ecdf4983e7633b32df94619d3 && git checkout -B drift-repro 272fec05bdf48e8dd06f2be252ef4aeb67740f41 && git merge --no-ff d4414410c797989ecdf4983e7633b32df94619d3

node scripts/docs-audit/affected-docs.mjs --json 272fec05bdf48e8dd06f2be252ef4aeb67740f41

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 272fec05bdf48e8dd06f2be252ef4aeb67740f41 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…e override write gate's docblock

The new step-2e helper sat between evaluateOverrideWriteGate and its own
docblock, stacking two docblocks and leaving the gate undocumented. Moved
unchanged; no behaviour change.

Claude-Session: https://claude.ai/code/session_01FEkQ5nSMgkteiemkoN31vf
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f82fd682510fca9bd7ab9727ba50ae299558048b
Local-runs: none
Scope: packages/spec/src/data/object.zod.ts + packages/objectql/src/registry.ts, packages/objectql/src/engine.ts, packages/metadata-core/src/extension-field-patch-targets.ts, packages/metadata-core/src/index.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/cli/src/commands/validate.ts, packages/cli/src/utils/field-patch-targets.ts, packages/spec/src/data/field-customization.ts, packages/spec/src/data/field-customization.test.ts, packages/spec/src/system/i18n-resolver.ts (scalarOverridesPackagedBase), packages/metadata-protocol/src/protocol-object-extender-fold.ts, packages/metadata-protocol/src/protocol-package-install.ts, packages/spec/src/kernel/manifest.zod.ts, docs/adr/0146 D10, docs/adr/0029 D9.2a, content/docs/data-modeling/object-extensions.mdx, .changeset/28565-field-patch-applied.md

Inputs read: card #28565 body and its three comments (unblock record, claim 6558511726, dev report 6560090896), PR #28728 body, its one bot comment, the 18-file net diff against main (merge-base d62d9737), and the check-runs on the head. The rest of the diff outside the hits file is the lane seat's ACCEPT at TIER_DEFAULT; it is read here only to judge the schema file against its first consumers.

① Derived judgments

  • Accept set of ObjectExtensionSchema (the hits file): unchanged. The diff moves only prose in object.zod.ts: the fieldPatches .describe() loses [EXPERIMENTAL — not enforced] and the N-23d forward reference, the TSDoc, the @example comment and the guidance code comment follow. FieldDisplayPatchSchema, whitelistedPatchShape, refuseExtensionFieldPatchKeys and the .strict() key set are untouched; nothing an author can write is added, refused or re-typed by the schema itself. Right.
  • Liveness verdict of data/ObjectExtension:fieldPatches flips experimental to live. The liveness ledger resolves experimental from exactly this marker (packages/spec/liveness/README.md:77), no packages/spec/liveness/*.json row names objectExtensions or fieldPatches (grep: none), authorable-surface/data.json:691 lists the key without a marker, and the Spec property liveness check-run on the head is green. The spec test that pinned the marker now pins its absence and the sentence naming mergeObjectDefinitions. Right: the consumer the marker waited for is in the same diff.
  • The sentence the description now publishes is true in the tree, clause by clause, checked against the cited code rather than the PR body:
    • "Applied by the registry merge (mergeObjectDefinitions) after the additive merges, on every object read": engine.ts:8993-9008 carries fieldPatches onto extDef at the one 'extend' registration (git grep on the head: one call site); registry.ts#mergeObjectDefinitions applies applyFieldPatches after the fields spread, the by-name rule merge, the index concat and the three scalar overrides; the fold foldExtendersOntoDefinition is the one implementation behind resolveObject (list read), resolveOwnerLayer (code layer) and foldObjectExtendersOnto (the by-name read in protocol-object-extender-fold.ts:60-62, unedited and correctly so). Right.
    • "a tenant's own text keeps winning": tenantPatchedDisplayKeys is computed once over the fold's base, compares the own contributor's field and option against the base's through scalarOverridesPackagedBase (the D9.2a predicate, imported, with the same two "no opinion" edges as tenantAuthoredScalars: no owner, base IS the owner by reference). Right, and faithful to ADR-0029 D9.2a's binding consequences (computed once over the base, never against the folded owner layer, no flag stamped).
    • "between two extensions the higher priority wins": contributors fold in ascending priority, the yield set does not move with the running merge, so the later (higher) write stands. Right; pinned by the registry test's "whichever registers first" case.
    • "A patch naming a field or option value the target does not have is refused at install and by os validate": install-local step 2e refuses 400 VALIDATION_ERROR with every refusal in details.refusals before any write (sits after the 2d override gate, before the hot-register); validate.ts step 3a-iv exits 1 with each refusal located. Both ask @objectstack/metadata-core's fieldPatchTargetRefusals, and the registry's boot warning asks fieldPatchTargetMisses, so the three cannot disagree on the verdict given the same target. Right. The brief's "both install doors" is honestly reduced to one: protocol-package-install.ts:230 parses with PackageInstallManifestSchema = ManifestSchema.extend(...), and ManifestSchema is a strictObject with no objectExtensions key (kernel/manifest.zod.ts:485), so no extension can reach that door; verified in the live tree, not taken from the PR body.
  • Public surface: @objectstack/metadata-core gains three exports (fieldPatchTargetMisses, fieldPatchTargetRefusals, fieldPatchTargetRefusalMessage) and a type pair; mergeObjectDefinitions (module-private) gains a fifth positional parameter and its one caller passes it; ObjectExtensionParsed and BASE_FIELD_DISPLAY_WHITELIST are consumed from @objectstack/spec/data, both already exported on main. Keys applied are read from the constant, so the whitelist stays the one list (ADR-0146 D10: "the whitelist the write gate reads, never a judgement the gate makes"). Right.
  • Picklist-bound fields: the fold skips options on a field with a non-empty picklist (the served options are the list's, written by finishServedObject after the fold), and the judge refuses such an option patch as picklist-option at both doors and in the boot log. The three agree, and the choice is the honest one: a label written in the fold would never be served. Right; the docs page says so in one sentence.
  • Idempotence of foldObjectExtendersOnto over a folded body: a base already carrying "Sector" reads as authored against the owner's "Industry", so the patch yields and the value stays — the same mechanism ADR-0029 D9.2a records for the scalars, and the same recorded cost: a tenant overlay written back verbatim from a served body freezes the extension's current patch text, and a package upgrade that changes the patch text does not reach that tenant. That edge is ruled and written into the ADR ("recorded here rather than papered over"); the card asked for exactly the scalars' yield. Right, not a finding.
  • Uninstall restores the vendor's text: the registered owner declaration is never mutated (applyFieldPatches copies on write, returns by reference when nothing applies), the fold recomputes from contributors, and the cache drops with the contributor. Right.

② Semver level

  • Changeset 28565-field-patch-applied.md: minor on @objectstack/objectql, @objectstack/metadata-core, @objectstack/cloud-connection, @objectstack/cli, @objectstack/spec — the five packages the diff moves, no package moved is missing, no package listed is unmoved (metadata-protocol is read, not edited, and is rightly absent). Metadata-protocol: semantic takes at least minor on each moved package (AGENTS.md Post-Task Checklist 3). Level: right.
  • Metadata-protocol: semantic is the card's own ruling (item 5: "a declared key gains its effect"). Read against the AGENTS precedence rule it could also be spelled narrowed (a value the install door admitted is now refused; "several ⇒ narrowed over semantic") — but the value it refuses was declared not enforced, the level is identical either way, and the card fixed the kind. Accepted as ruled; recorded, not a finding.
  • Clause-②: no, no arm, in both carriers: no published accept set is widened, and the refusal lands on a slot whose [EXPERIMENTAL — not enforced] marker was the published promise, so no (narrowing) arm is owed. The three new metadata-core exports are a public-surface addition that the line's own question (「扩大公开面吗」) could read as yes (widening); the seat's claim set no, and at this tier nothing turns on it. Recorded, not a finding.
  • check:adr-0087-registration is not owed (no breaking arm). Check Changeset on the head: green.

③ Boundary flags

Dev report deviations (6560090896), each answered:

  1. installPackage call line not written — verified in the live tree (PackageInstallManifestSchema extends a strict ManifestSchema with no objectExtensions key): a call there could never fire. Accepted; the card's "both install doors" reduces to install-local, and the record above says so.
  2. validate-build-gate-parity.test.ts edited outside the file surface — the closed roster requires the new validate-only gate to be classified; the row names the build-side gap honestly. Accepted.
  3. ObjectExtensionSchema.guidance.actions left unchanged "under ruling 3" — no ruling 3 exists in the card's comments (three comments: unblock, claim, report); the claim's file surface names "the N-23a slot's EXPERIMENTAL marker" only. The sentence is in the hits file and is now false by one key. Escalated as should-fix S2 below.
  4. protocol-object-extender-fold.ts needed no edit — verified (:60-62 calls registry.foldObjectExtendersOnto, the one apply path). Accepted.

open_questions: none declared. out_of_scope_findings: (b) os build runs no target check — recorded in VALIDATE_ONLY_GATES with its reason, outside the card's three doors, accepted as reported; (c) the skills doc names fields/priority only — Skills-docs: follow-up #28730 on the PR body. No PR comment raises anything (the one comment is the docs-drift advisory, which flags metadata-core/src/index.ts as un-anchored; the page that documents the new exports is object-extensions.mdx, which the diff updates).

④ Design judgment

packages/spec/src/data/object.zod.ts — judged against ADR-0146 D10 (second rule) and its first consumer, registry.ts#mergeObjectDefinitions, with the install door and os validate as the refusal's readers.

The design holds. ADR-0146 D10 asks for exactly three things and the tree does each: the patch is a per-field map in the extension body (the slot N-23a declared, carried byte-for-byte as ObjectExtensionParsed['fieldPatches'], no second shape anywhere — git grep fieldPatches on the head finds no other reader), mergeObjectDefinitions applies it after the additive merges, and the ADR-0029 D9.2a scalar yield applies to it through the one imported predicate. The description now published is a true account of the tree (① above), the marker flip is consistent with every consumer of the marker (liveness ledger, authorable surface, lint's experimental hint, the generated reference page, the spec test), and the boot path never refuses (AGENTS.md Startup registry reads): reportFieldPatchMisses warns once per object/package/field/option and the next fold applies a patch whose target has since arrived.

Attempts to break it, and what each found:

  • Tenant edit vs extension patch, including a re-fold of a folded body and a package upgrade changing the patch text — consistent with D9.2a in every case, including its recorded "verbatim write-back freezes the extender's text" edge. No finding.
  • Two extensions on one key — higher priority wins by write order over a yield set that does not move. No finding.
  • Picklist-bound fields — fold, install door, validate and boot log all answer "not patchable here". No finding.
  • Does the install-door judge and the validate judge agree with what the fold applies? — Not on a field another extension adds. The fold applies a patch to merged.fields as it stands when that extender's turn comes, so a patch on a field added by a lower-priority extender applies and one on a field added by a higher-priority extender is skipped and warned on every read (the docblock's "applies on the next read once the field arrives" is only true for the owner arriving later, not for this order). The install door judges against the registry's fully resolved object (every installed extender's field present, in any priority), so it admits such a patch. os validate judges against the composed stack's declared objects only (cli/src/utils/field-patch-targets.ts#declaredObjects), so it refuses the same patch when the adding extension is in the same stack. The same split applies to a patch on a field the same extension adds through fields: fold applies, both doors refuse. The published description says "the target object's own fields" (the conservative reading, under which validate is right and the other two are lenient), while registry.ts and object-extensions.mdx claim "reaches a field a lower-priority extender added". Three readers, two answers, on a case outside the sentence the spec publishes. Should-fix S1 — pick one: judge all three against the owner's own fields (drop the "lower-priority extender" claim from the registry docblock and the docs page, and have the install door read the owner layer), or apply patches in a second pass after every extender has merged and let declaredObjects compose extension-added fields. Not a must-fix: the spec's own sentence is kept true, validate green still implies install admits (validate is the strict one), and the card's tests all run on the owner's fields.
  • Fields in array vs record spelling across the three readers — the judge reads both spellings; the fold's recordFieldOf reads the record spelling only (the registry's merge contract: fields is spread). A body handed to foldObjectExtendersOnto with array-spelled fields is neither patched nor warned, because the warning is derived from the judge, which finds the field. Registry-registered bodies are records, so this is an edge of the public fold entry, not of a shipped read. Should-fix S3, small: have the fold warn by its own reader, or normalise once.
  • Removing the marker: every consumer — consistent (① above). The one stale sentence is in the hits file itself: guidance.actions still says "the merge carries fields, label, pluralLabel, description, validations and indexes only", while the code comment four lines above now lists fieldPatches. A refusal prescription an author is shown, now one key short. Should-fix S2, one-line edit; it is a sentence about actions, so nothing it refuses is wrong.
  • Changeset levels and declaration lines — ② above; no finding.

Findings:

  • Must-fix: none.
  • Should-fix S1: the three readers disagree on a patch naming a field an extension adds (fold: priority-order dependent; install door: admitted; os validate: refused); settle one answer and align registry.ts's docblock and object-extensions.mdx's "lower-priority extension" claim with the spec's "own fields".
  • Should-fix S2: ObjectExtensionSchema.guidance.actions in object.zod.ts lists six carried keys "only"; add fieldPatches.
  • Should-fix S3: the fold's miss report is derived from a judge that reads array-spelled fields while the fold itself patches record-spelled fields only; an array-spelled body through foldObjectExtendersOnto is silently unpatched.

Check-runs on the head at the time of this record: no failure, no cancellation; the lint, spec-liveness, changeset, queue-guard, docs and type-check source gates are green; the core and UI test shards, workspace type-check and E2E lanes were still in progress. Their conclusions are the gate verdicts and govern over this prose when they land.

Implemented-by: claude/project-thread-nye4b2
Reviewed-by: session_01FEkQ5oc56xChD6Wrc4cGSt

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

席位核对

ACCEPT · domain:server seat 5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt · 2026-11-01T16:10Z · head f82fd682 · #28565

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

席位意见:与 diff 相符。对象扩展里的 fieldPatches 现在真正生效:厂商字段的标签、帮助文字、占位符和选项标签可以被改写,类型和取值不动;租户自己改过的文字优先,两个扩展冲突时高优先级胜。补丁指向不存在的字段或选项,安装和 os validate 都会拒绝,启动时只记一条日志。

  1. Shape: draft against main, Fixes #28565, five packages minor (objectql, metadata-core, cloud-connection, cli, spec), Clause-②: no, Metadata-protocol: semantic, Skills-docs: follow-up #28730. check-governed-merges --pr 28728: NOT governed, +1334 / -31. git merge-tree against main: clean. Landing tier S.
  2. Against the order: the engine's extension copy carries fieldPatches; mergeObjectDefinitions applies the BASE_FIELD_DISPLAY_WHITELIST keys after the additive merges, yielding per key to tenant-authored text by the imported D9.2a comparison; one target judge in @objectstack/metadata-core read by the marketplace install door (400 VALIDATION_ERROR, details.refusals), os validate and the fold's once-only boot warning; the [EXPERIMENTAL — not enforced] marker removed and its spec test flipped.
  3. Accepted deviations: no installPackage call line (its PackageInstallManifestSchema is a strict manifest with no objectExtensions key, confirmed by the review in the live tree); a roster row in validate-build-gate-parity.test.ts for the new validate step. Rework on this head: the new install-door method moved above evaluateOverrideWriteGate's docblock (pure move); the PR body's Skills-docs: line rewritten to #28730 by the seat through the relay (body opens with the seat signature, as merged PR feat(spec,lint): a kanban board chooses which lanes it draws, and in what order (kanban.lanes) #28196).
  4. Gates: ## Contract review at CONTRACT_REVIEW_TIER on this head: PASS, comment 6560336731, no must-fix. Check-runs on this head: 59 success, 10 skipped, 0 failed (Test Core 1-8, Test UI 1-16, Type Check, Lint, Spec property liveness and Check Changeset green). Dev: --ran 114/114 derived families.
  5. Out of scope: filed fieldPatches: make the fold, the install door and os validate agree on a patch of a field another extension adds, and fix the stale actions guidance list #28733 for the review's three should-fix findings (the fold, the install door and os validate disagree on a patch of a field another extension adds; the stale guidance.actions list; array-spelled fields in the fold), pm:blocked on 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. Filed skills(objectstack-data): teach an object extension's fieldPatches in the Object Extension Model section, once #28565 applies them #28730 (skills objectstack-data teaches fieldPatches), pm:blocked on 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.
  6. Landing: pr_ready + automerge_enable through the relay.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 16:11
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 16:11
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Nov 1, 2026
…-nye4b2

The overlap with the __c extension-field change in packages/cli is resolved
by keeping both: os validate runs both ADR-0146 D10 steps in main's order
(3a-iv the __c reservation, then 3a-v the field-patch target check), and
VALIDATE_ONLY_GATES keeps both rows.

Claude-Session: https://claude.ai/code/session_01FEkQ5nSMgkteiemkoN31vf
Co-authored-by: Claude <noreply@anthropic.com>
… their siblings carry

Both cases spawn the real CLI; under a loaded box the 5 s default timed out
(5014 ms / 5001 ms on the post-merge run). The sibling validate spawn tests
(validate-extension-field-suffix, validate-override-declarations) pass 120_000.

Claude-Session: https://claude.ai/code/session_01FEkQ5nSMgkteiemkoN31vf
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d4414410c797989ecdf4983e7633b32df94619d3
Local-runs: none
Scope: packages/spec/src/data/object.zod.ts + packages/cli/src/commands/validate.ts, packages/cli/test/validate-build-gate-parity.test.ts, packages/cli/test/validate-field-patch-targets.test.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/objectql/src/engine.ts, packages/objectql/src/registry.ts, packages/metadata-protocol/src/protocol-extension-field-suffix.ts, packages/metadata-protocol/src/protocol.ts (the save door's VALIDATION_ERROR envelope), packages/spec/src/api/errors.zod.ts (HttpStatusErrorCodeMap)

This record is a DELTA of the earlier record on this PR (comment 6560336731, head f82fd682): everything that record judged and does not name here stands unchanged, its three should-fix findings (S1 fold/install/validate disagreement on an extension-added field, S2 the stale guidance.actions sentence, S3 the record-only fold reader) included — none of them is addressed in the delta. Read: the net diff against main at this head (18 files, merge-base db357f6d), the delta f82fd682..d4414410 restricted to the PR's files, and the check-runs on the head. Nothing posted.

① Derived judgments

  • The hits file is unchanged. git diff f82fd682 d4414410 -- packages/spec/src/data/object.zod.ts is empty, and the net hunk against the new merge-base is byte-identical (same digest, index lines excluded) to the one judged at f82fd682. The earlier ① judgments on the accept set, the liveness flip and the published sentence hold as written; Spec property liveness is green on this head too.
  • Every other net hunk of this PR is byte-identical to the earlier head for registry.ts, metadata-core/src/extension-field-patch-targets.ts, metadata-core/src/index.ts, cli/src/utils/field-patch-targets.ts, object-extensions.mdx, the changeset and the spec test (digest-compared per file). engine.ts's net hunk is the same five-line fieldPatches: ext.fieldPatches carry; the other engine.ts lines in the delta (isUnparseableDate, UNPARSEABLE_DATE_FAULT) are main's own and are not in the net diff. Right.
  • The merge kept both ADR-0146 D10 features intact at all three readers, judged on the tree:
  • VALIDATE_ONLY_GATES holds both rows (findExtensionFieldSuffixRefusals from feat(metadata-protocol,cli,cloud-connection)!: __c extension fields only in a base's paired .custom package, judged at every door (ADR-0146 D10) #28724, findFieldPatchTargetRefusals from this PR), each with its reason, and the ledger's docblock was reworded to say it holds two. The closed roster is satisfied by the tree, not by a merge marker. Right.
  • d4414410c gives the two field-patch validate spawn tests a 120_000 budget; the sibling spawn file validate-extension-field-suffix.test.ts on main carries the same figure on each of its cases. Test-only, no accept-set or surface change. Right.

② Semver level

Unchanged from the earlier record: minor on the five moved packages, Metadata-protocol: semantic as the card ruled, Clause-②: no with no arm. The merge adds no package to the diff and moves none. Check Changeset was still running on this head when this record was written; its conclusion governs.

③ Boundary flags

The dev's one new flag, judged against the tree rather than taken on trust:

  • Two VALIDATION_ERROR refusals at one door, two statuses. feat(metadata-protocol,cli,cloud-connection)!: __c extension fields only in a base's paired .custom package, judged at every door (ADR-0146 D10) #28724's 1c'' answers VALIDATION_ERROR / 422 (UNRUNNABLE_REFUSAL_CODE / UNRUNNABLE_REFUSAL_STATUS, this door's own body-refusal envelope, the one its unrunnable-code refusal and PLUGIN_REGISTER_FAILED already use); this PR's 2e answers VALIDATION_ERROR / 400. What the two reference points say: the save door (protocol.ts and protocol-extension-field-suffix.ts#extensionFieldSuffixRefusalError) envelopes VALIDATION_ERROR as 400; main's install door uses 400 only for INVALID_REQUEST, RESEED_SKIPPED and NOTHING_TO_PURGE and 422 for every body refusal that carries VALIDATION_ERROR; the spec's HttpStatusErrorCodeMap names 400 → VALIDATION_ERROR and lets 422 fall into the same code by bucket. So 2e matches the save door and the spec's table, 1c'' matches the door it sits in, and the two now sit twenty lines apart answering one code with two statuses. A client branching on error.code sees one answer; one branching on status sees two for one kind of fault at one door. Neither the card nor the spec fixes the status, the 2e test pins 400 and feat(metadata-protocol,cli,cloud-connection)!: __c extension fields only in a base's paired .custom package, judged at every door (ADR-0146 D10) #28724's pins 422, and nothing admitted or refused changes with the number. Should-fix S4, not must-fix: one door, one status for one code — the natural move is 2e to UNRUNNABLE_REFUSAL_STATUS (422), the door's own envelope, with its test pin following; a recorded reason to keep 400 would also close it. Escalated here, not a FAIL.

No other deviation or open question is new. The earlier record's answers to deviations 1–4 stand.

④ Design judgment

packages/spec/src/data/object.zod.ts — no change in the delta; the earlier ④ stands, its findings unchanged. The delta added one thing worth trying to break: whether main's __c reservation and this PR's target check compose at the three readers. They do: the suffix judge reads objects and objectExtensions[].fields and never fieldPatches; the target judge reads fieldPatches and never a field's name spelling; at os validate the two run in sequence and the first refusal exits, at the install door 1c'' precedes 2e and both precede every write, and in the fold the registry's __c handling (none in registry.ts; the reservation lives at the doors) leaves applyFieldPatches as judged before. A .custom extension patching region__c's display is admitted by both judges wherever region__c exists on the target; that is the earlier S1 edge (a field an extension adds) and nothing in the merge moves it.

Findings on this head:

Check-runs on the head at the time of this record: 34 runs — 11 success (Auto Label, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, PR-body guards, Spec property liveness, filter), 7 skipped, 16 in progress (Build Core, Build Docs, Build UI Deps, Check Changeset, the ESLint / repo / docs / CI-release / scoped lint lanes, the four Type Check lanes, Plan Test Core shards, Live E2E UI), 0 failure, 0 cancelled. Their conclusions are the gate verdicts and govern over this prose when they land.

Implemented-by: claude/project-thread-nye4b2
Reviewed-by: session_01FEkQ5oc56xChD6Wrc4cGSt

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

席位核对 · re-land

ACCEPT · domain:server seat 5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt · 2026-11-01T17:49Z · head d4414410 · #28565

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

席位意见:与上次 ACCEPT(6560471454,head f82fd682)相同;本次只是合并 main 解决与 #28724 的冲突,外加一处测试超时预算。

  1. Shape: unchanged; the merge queue dropped the PR at f82fd682 with MERGE_CONFLICT against feat(metadata-protocol,cli,cloud-connection)!: __c extension fields only in a base's paired .custom package, judged at every door (ADR-0146 D10) #28724 (447a2a6f6). git merge-tree against current main: clean.
  2. Against the order: the delta is merge 844403abc (both ADR-0146 D10 validate steps kept, feat(metadata-protocol,cli,cloud-connection)!: __c extension fields only in a base's paired .custom package, judged at every door (ADR-0146 D10) #28724's __c at 3a-iv and field-patch at 3a-v; both VALIDATE_ONLY_GATES rows; the install door carries both judges) and d4414410c (the two field-patch validate spawn tests take the 120 s budget their siblings carry).
  3. Accepted deviations: as before.
  4. Gates: delta ## Contract review at CONTRACT_REVIEW_TIER on this head: PASS, comment 6561400798, no must-fix. Check-runs on this head: 57 success, 8 skipped, 0 failed. Dev: --ran 114/114 on the merged tree.
  5. Out of scope: the delta review's new S4 (step 2e answers VALIDATION_ERROR 400 where the same door's other body refusals answer 422) is added to fieldPatches: make the fold, the install door and os validate agree on a patch of a field another extension adds, and fix the stale actions guidance list #28733 as item 4 (comment 6561405801).
  6. Landing: pr_ready + automerge_enable through the relay.

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit e62dcc5 Nov 1, 2026
65 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-nye4b2 branch November 1, 2026 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/xl tests tooling

Projects

None yet

1 participant