Repository navigation
feat(metadata-protocol,objectql): an object extension is an authorable item of a writable package, saved, published and exported through the existing doors (#28574) - #28811
Conversation
…e item of a writable package `object_extension` joins the metadata type registry. A writable package saves, publishes, restores and deletes one through the existing /meta doors; the gate refuses an unbound write, the registry's extend refusals, a name other than the target, an unknown or out-of-closure target and a `.custom` field without `__c`. Every stored-row sync registers the whole parsed body as the package's 'extend' contributor and delete/uninstall withdraw it. The package export carries the rows as `objectExtensions` and no longer exports an extended object as the package's own. Co-Authored-By: Claude <noreply@anthropic.com>
…me the item gate in the __c judge's door list Regenerates the metadata-plugin reference page the merge left stale. Co-Authored-By: Claude <noreply@anthropic.com>
…its code files follow Every registry entry declares at least one file pattern (metadata-plugin registry pin); an empty list also reads as "no pattern" to the file-system loader. Co-Authored-By: Claude <noreply@anthropic.com>
…e object_extension registry row Co-Authored-By: Claude <noreply@anthropic.com>
…ir in zh-CN, ja-JP and es-ES The bare-type ledger and the label positive control count the new registry type, as they did for queue and assignment_rule. Co-Authored-By: Claude <noreply@anthropic.com>
…phans door Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 6 package(s): 48 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 15 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 173 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9856aae0c042d8629af437e697bff744d72ed416 && git checkout 9856aae0c042d8629af437e697bff744d72ed416
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 693e1003c981789896ef7084109d2e13a7457e67 d05c186366d671c85a53069bbbde24dff6bad0b7 && git checkout -B drift-repro 693e1003c981789896ef7084109d2e13a7457e67 && git merge --no-ff d05c186366d671c85a53069bbbde24dff6bad0b7
node scripts/docs-audit/affected-docs.mjs --json 693e1003c981789896ef7084109d2e13a7457e67
|
…within one derivation Adding object_extension made deriveReferenceSites() walk the FieldSchema record and the validation-rule array a second time: object and object_extension declare the same `fields` and `validations`, and the `validations` walk alone is most of a type's cost. The derivation test's 5s budget went red in CI (5532ms). A root property's walk now runs once per derivation for every source type declaring the same property, and later types replay its bindings. The key is the property's schema less its prose (string `description`/`title`, which no step reads), its trail and the root's `$defs`. The cache lives in one derivation, so two calls still agree; the index equals the uncached walk's exactly, and against main it adds only object_extension's four sites (field, fields, object, picklist). Measured, ms per derivation, 6 runs per round, shared box: - main: 431 423 425 462 535 473 / 407 422 396 461 377 380 - head before: 680 875 865 783 759 603 / 666 664 726 674 698 853 - head after: 447 473 408 482 379 373 / 415 401 390 420 413 381 - main, same session as "after": 416 402 377 417 369 380 / 477 571 401 424 419 402 No timeout changed and no test skipped. Co-Authored-By: Claude <noreply@anthropic.com>
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: Taken under Generated by Claude Code |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract reviewServed-tier: Inputs: card #28574 and its four comments; ADR-0146 on ① Derived judgments
② Semver levelMinor on Declarations: Check-runs on the head: every run concluded ③ Boundary flags
④ Design judgment
Scenarios, each read against the code:
Findings:
No must-fix. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC 席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #28574. 22 files. The spec hunk is the object_extension registry row, the enum member and its override classification (shape A, one PR as placed), plus filePatterns ['**/*.extension.ts'], which the registry pin requires (the review judged it correct), and the regenerated URL spelling. The item doors (save, publish, restore, delete) run the gate and the #28564 __c judge unchanged. Registration as 'extend', withdrawal, and the export leg. Translations for the new type. |
| Fix since hand-back | ccfa0893 timed out in reference-sites.derivation.test.ts. d05c1863 caches root-property walks inside one deriveReferenceSites call: the cost is back to main's, the derivation stays pure, and the index is identical. No timeout change and no skip. |
| Should-fix | Findings 1, 2 and 4 are in cited, pre-existing code, so the seat files them as follow-up cards. Finding 3, the registry row comment, rides the follow-up that touches it. None is a blocker. |
| Evidence | CI green on head d05c1863 (filter=latest: 57 success, 8 skipped, 0 failed). The report shows the card's tests red on main (18/19) and green on head, three ablations each restored clean, the six touched packages' full suites, 117/117 derived gates, and a live showcase probe. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, with every check green or an expected skip. |
Generated by Claude Code
Requested by Leehom · project thread
Fixes #28574
Clause-②: yes (widening)
Metadata-protocol: added
维护者速读
object_extension,即「给别的包的对象加字段和规则」)成了可写包里的普通元数据项:通过现有的/api/v1/meta草稿、发布、历史回滚和删除口保存;发布后整个扩展体注册为该包的extend贡献者,字段出现在对象的每一种读取里,表也加上列;删除或卸载包时撤回;包导出把它带成objectExtensions,可以再装回去。.custom包、字段带__c后缀、基础包不动(ADR-0146 D10)。以前这类数据只能由代码包的 manifest 带进来,运行时没有保存口。object_extension的行;manifest 里的objectExtensions注册方式不变。顺带修正导出:一个包扩展了别人的对象时,导出不再把那个对象当成自己的对象输出(以前再安装会被所有权冲突拒绝)。回滚 = revert 本 PR。packages/metadata-protocol/src/protocol-object-extension-item.ts(保存/发布/回滚口的拒绝顺序与注册体);②packages/metadata-protocol/src/protocol.ts里registerStoredObjectExtension/resyncStoredObjectExtensions和列表合并listSlotNameOf那几处;③packages/runtime/src/domains/packages.ts的导出循环(objectExtensions一腿和objects只留自有对象的过滤)。pnpm --filter @objectstack/runtime exec vitest run src/domains/meta-object-extension-item.test.ts,预期 19 个测试全绿。What changes
packages/spec, the one hunk seat 1 ruled, shape A):object_extensionjoinsMetadataTypeSchemaandDEFAULT_METADATA_TYPE_REGISTRY(allowRuntimeCreate: true,supportsVersioning: true, no overlay,domain: 'data',filePatterns: ['**/*.extension.ts'], the convention of the one code file that holds an extension,examples/app-showcase/src/data/extensions/account.extension.ts; the registry pinmetadata-plugin.test.tsrequires a pattern on every entry), plus the one classification row thesatisfies(a record over everyMetadataType) onOVERRIDE_TYPE_CLASSIFICATIONcompile-forces:object_extension: { disposition: 'excluded', basis: 'adr_0146_d10_extended' }. Regenerated:meta-url-data.generated.ts(gen:meta-url-spelling) and the two reference pagescheck:generatednamed (gen:docs). No other spec key, no new error code,ObjectExtensionSchemauntouched. The route ledger, SDK and OpenAPI did not move.examples/app-showcase/src/coverage.tsgains theKIND_COVERAGEentry the closed kind roster requires (admitted by the same ruling).packages/metadata-protocol/src/protocol-object-extension-item.ts(new), asked by the save door, the per-item publish door and the history restore (protocol-authoring-gate.ts), beside the__cjudge it calls.VALIDATION_ERROR/ 400 with an issue path, in this order: a write bound to no package; the registry's owndefaultRowScope/membershipKeysrefusals with their sentences (extendContributionRefusal, now one exported function inobjectql/src/registry.tsthatregisterObjectalso calls); a row filed under a name other than itsextend; an unregistered target; a target outside the written package's closure (narrowObjectsToPackageClosure, withclosureDependencyRemedy's prescription); the__cjudge from metadata-protocol + cli + install-local:__cextension fields only in a.custompackage — refused at save/publish/restore,os validate, both install doors and duplicate #28564 (judgeExtensionFieldSuffix), unchanged. The body's shape is the doors' own schema gate (resolveOverlaySchema→ObjectExtensionSchema, strict, over the body with the ADR-0010 envelope peeled off): 422INVALID_METADATA.registerStoredObjectExtensionregisters the WHOLE parsed body (ObjectExtensionSchemaoutput,_provenance: 'org') throughSchemaRegistry.registerObjectExtension→registerObject(..., 'extend', priority). Called from the mutation sync (applyRegistryWriteThrough) and the stored-row hydration every boot / reload path shares (hydrateOverlayIntoRegistry). The manifest loop inengine.tsis not touched: what a manifest extension registers is unchanged.restoreArtifactRegistryView(the delete and uninstall heal) callsresyncStoredObjectExtensions(target): it withdraws every stored-row (_provenance: 'org')extendcontributor of the target whose package has no active row left (SchemaRegistry.withdrawObjectExtension, new) and re-registers the rows that remain. Code-package contributors are never withdrawn.ensureObjectStoragesyncs the target's table on anobject_extensionpublish, so a record can store the new field without a restart.object_extensionbody carriesextendand noname(its schema refuses one), somergePackageAwareOverlayslotted none of them and the list and per-package reads answered[].listSlotNameOfslots such a body by its target. The body is served as stored: nonameis added, so what GET answers PUTs back through the strict schema.assemblePackageManifest(packages/runtime/src/domains/packages.ts) reads the package'sobject_extensionitems (per-package layer) intoobjectExtensions. Measured on base7a48bf73: it did NOT carry them (the base-red export test). And a package'sobjectskeep only the objects it owns: the registry lists an object under every contributing package, so an extending package exported the target as its own object, and re-install refused it withObjectOwnershipConflictError.reference-sites.tswalksobject_extensionwithObjectExtensionSchema(the type is deliberately not inBUILTIN_METADATA_TYPE_SCHEMAS), which keeps theunwalkableSourceTypespin at exactly['external_catalog']instead of widening it.packages/platform-objects): the metadata-forms bundles are generated from the registry, so the type'slabel/descriptionarrive in every locale (check-i18n-bundles --write). Authored in zh-CN (对象扩展), ja-JP (オブジェクト拡張) and es-ES (Extensión de Objeto) on arrival, asqueueandassignment_rulewere (pnpm i18n:extractthen dropped the six provenance rows). The two translation ledgers that count the registry move by the new type:bare-type-display-echo-decisions.test.ts(31 → 32 types, 14 → 15 bare, 24 → 26 leaves) and the label positive control inobject-lifecycle-panel-echo-decisions.test.ts(697 → 698), each with its one-line history entry.__cjudge's door list (protocol-extension-field-suffix.tsheader, prose only): names theobject_extensiongate and the adopt-orphans door fix(metadata-protocol)!: the adopt-orphans door asks the__cjudge against the target package; the promotion's suffix block is seal-gated #28768 added (seat 1's note from that PR's contract review).@objectstack/lint/runtimere-exportsclosureDependencyRemedy(one line), so the gate prints the closure sentence the other save doors print instead of re-spelling it..changeset/28574-object-extension-package-item.md: minor onspec,lint,objectql,metadata-protocol,runtime,platform-objects.Tests
packages/runtime/src/domains/meta-object-extension-item.test.ts(new, 19 tests) over a real better-sqlite3SqlDriver, a realObjectQLengine, the real protocol andHttpDispatcher.handlePackagesfor the export. Fixture: code basecom.acme.crmowningcrm_account, its paircom.acme.crm.custominstalled throughinstallPackage, and an unrelatedcom.acme.other.All readings at head
ccfa0893(after mergingorigin/mainwith #28768 in), unless named.Tests 19 passed (19)(lock VERDICT command-exit 0)8afaf12d(every production file of this diff restored to the merge base, the new test kept, five packages rebuilt)Tests 18 failed, 1 passed (19). On base the type does not exist, so every save answers'object_extension' is not a metadata type; the one green is the read-only-base control, refused on base too (400 for the unknown type). Restore:git checkout HEAD -- .,git diff HEADempty, rebuilt.The 19 tests: the field on the by-name, list, layered and registry reads with the pair as its only
extendcontributor and the row stored; a draft not live until publish; a record storing a value in the new column; the extension'svalidationsreaching the object (the whole body is registered); a boot (loadMetaFromDb) re-registering the row; the pair's export carryingobjectExtensionsthat parse; the base's export carrying none; that export re-installing into a fresh kernel throughregisterApp; delete and uninstall each removing the field from every read (each asserts it was on every read first); seven refusals, eachVALIDATION_ERROR/ 400 with its issue path where it has one, leaving no row and no contributor (outside the closure, unknown target, a.customfield without__c,defaultRowScope,membershipKeys, a package-less write, a name other than the target); the read-only base refused; and theexcludeddisposition not closing the type to the pair.Ablations (one per door call,
node scripts/ablation-replace.mjsfrom committed HEAD, package rebuilt andablation-dist-preflightmarker found in dist before each run)objectExtensionItemRefusal's type check rewritten to returnundefinedalways (markerABL1): blob796dce22cb1c→14a8016fdac0.Tests 7 failed, 12 passed (19): exactly the seven refusals. Restored: blob equals HEAD,git diff HEADempty.'extend'registration.registerStoredObjectExtension'sregistry.registerObjectExtension(...)call guarded off (markerABL2): blobbf4d4c0f79a5→cc5b2b90db49.Tests 7 failed, 12 passed (19): the reads, draft-then-publish, record, validations, boot, delete and uninstall tests. Restored clean.objectExtensionsdropped from the export loop (markerABL3): blobeec70a607e34→ffbfc47a15cc.Tests 2 failed, 17 passed (19): the export test and the re-install test. Restored clean; runtime rebuilt,--absentpreflight green, card file 19/19 again.ABL2(the ablation tool restores source only); it was discarded and re-run alone on a rebuilt dist. The reading above is that second run.Full suites and typecheck at head
ccfa0893(each under the verify lock, VERDICT command-exit 0)@objectstack/metadata-protocol@objectstack/objectql(local)@objectstack/runtime(local)@objectstack/spec@objectstack/lint@objectstack/platform-objectsregistry-extension-system-columns.test.ts+registry-object-extension-nonenumerated-prop-discard.test.tstest/coverage.test.ts; cligenerate-file-name-registry-parity.test.tstsc --noEmitspec, lint, objectql, metadata-protocol, runtime, showcasecheck:type-check-debtre-measured at head: 0 errors)eslint --no-inline-configover the 11 changed TS filesDerived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat head: 117 families. All 117 exit 0;--ran: 117 derived, 117 run, 0 NOT-MEASURED, 0 UNRUN. On the way:check:i18nred until the translation bundles were regenerated for the new registry row;check:query-options-erasurered on twoas anycasts in the new test, removed;check:generated/check:docsred after each merge untilgen:docs;check-plugin-teardown-shape --self-testexit 3 on the shallow clone until its fixture commit was fetched. No gate was added.Acceptance notes
/meta/typesserves no JSON schema forobject_extension, and a draft carries no_diagnosticsbadge: the type is not bound inBUILTIN_METADATA_TYPE_SCHEMAS, because that map requires the ADR-0010 envelope, which would change the schema a manifest'sobjectExtensionsparse with.excludeddoes not close the type to a pair.OVERRIDE_TYPE_CLASSIFICATIONlistsobject_extensionasexcluded(it never replaces a base item, so it never carriesoverrides); the pair's save is admitted regardless. Pinned by the last test.subtractExtenderContributions. The order and card namesubtractExtenderContributionsfor delete. Measured, that is the fold's idempotency helper (it removes from a base theindexesan extender is about to re-add,registry.ts); it removes no contributor.withdrawObjectExtensionsplices the package'sextendcontributor and drops the merged-object cache entry.registerApp, the path the export is assembled for.assemblePackageManifest's own docblock names it:marketplace-install-local→manifestService.register()→engine.registerApp().installPackage(POST /api/v1/packages) parsesPackageInstallManifestSchema, which refuses every collection key butobjects(measured on spec dist at head:views,flowsandobjectExtensionsall answer "Unrecognized key(s)"), so no export carrying views or flows was ever aninstallPackageinput. No second code path was written.objectsis a defect this card makes reachable (an extending package's export), fixed where the extension leg is added.packages/lint/src/runtime.ts(one re-export line),packages/metadata-protocol/src/reference-sites.ts(the walk above), theprotocol-extension-field-suffix.tsheader (prose), the regenerated reference pages and translation bundles, and the two translation-ledger count pins. Each is forced by the registry row or asked by seat 1.object_extensionsave is refused by the gate's first step (pinned). Whether the adopt-orphans door should judgeobject_extensionrows it rebinds is left open in the report, not widened here.BUILTIN_METADATA_TYPE_SCHEMAS(JSON schema and_diagnostics), which needs a ruling on the envelope. Recorded in the report.__cjudge against the target package; the promotion's suffix block is seal-gated #28768 (metadata-protocol: the adopt-orphans door rebinds a package-lessx__cfield into a plain package without the__cjudge (#28564 follow-up) #28735) landed while this was in flight.origin/mainwas merged three times (after 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 / feat(service-automation, metadata-protocol): a flow override declared in BASE.custom is the packaged flow, saved, served and armed in place of the base (#28412) #28693, after main moved ten more commits, and after fix(metadata-protocol)!: the adopt-orphans door asks the__cjudge against the target package; the promotion's suffix block is seal-gated #28768), each with no conflict, and every reading in this body was retaken after the last one.registerObjectExtensionhands the whole body, sofieldPatchesreaches 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's apply unchanged.Verification
Live probe on the stock showcase at head
ad2bfc9c(this diff merged withorigin/mainonce; the later commits change only tests, translations, prose and the file pattern),pnpm dev -- --fresh -p 3917inexamples/app-showcase, signed in as the seeded admin (POST /api/v1/auth/sign-in/emailwithOrigin: http://localhost:3917, cookie jar).B=http://localhost:3917/api/v1.Two-minute check:
pnpm --filter @objectstack/runtime exec vitest run src/domains/meta-object-extension-item.test.ts, 19/19.Generated by Claude Code