Repository navigation
fix(objectql)!: withhold a disabled package's objectExtensions fields from the object they extend, and keep their columns - #30362
Conversation
…e served fold Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbdPwGwxWkDNgrE4LE
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbdPwGwxWkDNgrE4LE
…ved object and kept in storage Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbdPwGwxWkDNgrE4LE
…thheld from the served object Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbdPwGwxWkDNgrE4LE
…the governance inventory keeps the served one Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbdPwGwxWkDNgrE4LE
📓 Docs Drift CheckThis PR changes 3 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 60 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 77a0afd77bac2334ae6ff88f473dc6e1940544c3 && git checkout 77a0afd77bac2334ae6ff88f473dc6e1940544c3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 870ebce333bf0a5794307febe077ab5652cfbfe9 81f7ce8a12d8ddc8231c9a1f7c86b509fed46a2d && git checkout -B drift-repro 870ebce333bf0a5794307febe077ab5652cfbfe9 && git merge --no-ff 81f7ce8a12d8ddc8231c9a1f7c86b509fed46a2d
node scripts/docs-audit/affected-docs.mjs --json 870ebce333bf0a5794307febe077ab5652cfbfe9
|
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF Contract-review-claim: session_01Utp6CZMkHvuDeVYgTD73qF · head 12d54cf · 2026-11-05T08:00Z |
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF Contract reviewServed-tier: Inputs: card #30304 (body, triage 6650917536, claim 6651772742, dev report 6652852213), PR #30362 (body, 8-file list, 3-dot diff against ① Derived judgmentsEach accept-set or public-surface change the diff implies, read in the head tree:
② Semver level
③ Boundary flags
④ Design judgmentThe narrowing itself, against the published spec on Must-fix 1 — a DDL-free Should-fix 1 — Should-fix 2 — the roll-up index is a storage-shaped reader on the served fold. Should-fix 3 — open question 1 (cross-package rule reading a withheld field). Read in Tried and held. (a) Column/value loss: boot sync, DDL-free registration, federated binding, Gates. The check-runs on Implemented-by: VERDICT: FAIL Rendered 2026-11-05T08:24Z on head |
… and a withheld extension's indexes leave a folded base Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbdPwGwxWkDNgrE4LE
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbdPwGwxWkDNgrE4LE
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF Contract-review-claim: session_01Utp6CZMkHvuDeVYgTD73qF · head f2ce2c0 · 2026-11-05T08:44Z |
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF Contract reviewServed-tier: Delta review of record 6653307077 (head ① Derived judgmentsJudgments 1–9 of record 6653307077 stand on this head unchanged (the delta touches none of the served folds' predicate, the storage readers in objectql/runtime, the cache invalidation points or
f2ce2c0 narrows a Other storage-shaped readers in the CLI migrate path, swept as asked. ② Semver level
③ Boundary flags
④ Design judgmentThe narrowing itself is unchanged and holds as judged in 6653307077: no published schema declares that an
Gates. The check-runs on Implemented-by: VERDICT: PASS Rendered 2026-11-05T09:12Z on head |
…er misreports Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbdPwGwxWkDNgrE4LE
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF 席位意见:ACCEPT(#30304)。包停用后,它通过 席位核对Head-sha:
|
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF 维护者速读
席位意见:ACCEPT(#30304)。包停用后,它通过 席位核对Head-sha:
|
Requested by Bill · project thread
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
维护者速读
Fixes #30304
Clause-②: yes (narrowing) — while a package is disabled, the served object it extends (
GET /api/v1/meta/object/:name, the record page, the/datafield map) no longer carries the fields, validation rules and indexes the package'sobjectExtensionsadd, so a/datawrite, filter, sort orfieldsprojection naming one is refused as a field the object does not have, andGET /api/v1/packages/:id/customizationsno longer lists them among the base'scontributions.Metadata-protocol: semantic
What changed
packages/objectql/src/registry.ts— every served fold leaves out theextendcontributions of a disabled package (isPackageDisabled, the predicate objects, flows, navigation and actions already use):resolveObject(behindgetObject/getAllObjects), the per-package fold (withoutExtensionsOf/onlyExtensionsOf) andfoldObjectExtendersOnto(the protocol's by-name read over a metadata-service body);getStorageObject(name)/getAllStorageObjects(): the fold with every contribution, disabled or not. With nothing disabled they are the served answer (same cache, same walk);installPackagedrops cached folds when the row lands disabled (a fold cached before the row existed read the package as enabled).ObjectQLPluginboot sync,registerSchemasWithoutDdland the federated reconciliation (readRegisteredObjects(seam, 'storage')); the governance inventory keeps the served read;ObjectQL.syncSchemas/syncObjectSchema;getAllStorageObjectswhen the registry has it), so the archive datasource's schema keeps every column the hot rows carry;os migrate's composed coverage (measureComposedCoverage,packages/cli/src/utils/schema-migration-plugins.ts): its DDL-free branch (OS_SKIP_SCHEMA_SYNC) installs each object as the driver's managed field set withregisterObjectMetadata, so on the served fold the drift report proposed dropping every column a disable only hides. It readsgetAllStorageObjects(falling back togetAllObjects).packages/runtime/src/domains/packages.ts—readBaseContributions'fieldloop skips a disabled package's contributor, as itsnavloop does. This makes the spec docblock spec(package-lifecycle): the customizations read's docblock says a disabled package's contributions are never listed, but its fields are listed and served #30086 names (package-lifecycle.zod.ts"a disabled package's contributions MUST NOT be listed, as the fold drops them") true for fields. The array-level describe there still names only flows and navigation; that text is the spec lane's (spec(package-lifecycle): the customizations read's docblock says a disabled package's contributions are never listed, but its fields are listed and served #30086), not edited here.Measured, not assumed
setPackageDisabled(environmentId, …), one registry row per kernel); there is no separate org-level disable to keep in step. Pinned through the real door, including a boot that replays the state file.getAllObjects). With the served fold narrowed and sync left on it, a boot that starts with the module disabled creates the table without the module's columns, and a write after enable answers 500 (measured in the ablation below). Hence the storage fold.o2c_probe_totalshape). The roll-up index is built from the served fold, so a withheld summary is not recomputed while disabled; its child object is refused (OBJECT_PACKAGE_DISABLED) anyway. Re-enable serves the stored value (pinned: 2000 back).null(200, no 5xx).fieldsnaming a withheld field: byte-identical to naming an unknown field (400 INVALID_FIELD/INVALID_SORT), pinned by comparison.400 INVALID_FIELD,Unknown field '…' on object '…'), create and update. No new code.Tests
packages/runtime/src/domains/packages-disable-module-extension-fields.test.ts— real doors (HttpDispatcher→/packages/:id/disable|enable,/meta/object,/data,/packages/:id/customizations) over a file-backed SQLite: control, disable, unknown-field parity, other-field writes, contributions list, re-enable with values, disabled-at-boot column creation, and the cross-module formula / rule / query measurements.packages/objectql/src/registry-disabled-package-extensions.test.ts— the four served readers, the storage readers, a folded base and a tenant overlay (fields, rules and indexes), a row that lands disabled after a cached fold.packages/cli/src/utils/schema-migration-plugins.test.ts— a top-level describe over a real SQLiteSqlDriverand a realSchemaRegistry: a table created with the module enabled, the module disabled,measureComposedCoverage(…, { ddlFree: true }), thendetectManagedDrift()proposes no drop of the hidden column and the stored value stays.Ablations (one-time, via
scripts/ablation-replace.mjs, restored toHEADwithgit diff HEADempty; dist restore checked withablation-dist-preflight --absent):withholdDisabledExtensionsalways answersundefinedsyncSchemasread the served fold (objectql rebuilt)500readBaseContributionsfield loop without the disabled skipmeasureComposedCoveragereadsgetAllObjects(served)expected [ 'drop_column:qm_credit' ] to deeply equal []Gates (at
f2ce2c0f7)node scripts/pm/dispatch-gates.mjs --commandson this diff: 65 families, all run, all exit 0.--ran: 65 derived, 65 run, 0 NOT-MEASURED.pnpm --filter @objectstack/cli typecheck/@objectstack/objectql typecheck/@objectstack/runtime typecheck: exit 0 (cli's test layer holds its ledger: 28 errors / 6 pinned signatures, unchanged).pnpm lint: exit 0 (at12d54cf31, before the review fixes).@objectstack/objectqlfull suite before the merge oforigin/main: 552 files, the one red file (plugin-registry-read-failure.test.ts, 5 cases) fixed by scoping the storage read to the DDL seams. Atf2ce2c0f7: the registry pins andplugin-registry-read-failure.test.tsgreen.@objectstack/clischema-migration-plugins.test.tsand the runtime route pin: green atf2ce2c0f7.test:repo, dogfood, the whole-tree families the derivation leaves to CI.Contract review
Round 1 FAIL (6653307077): M1 (
measureComposedCoverageon the served fold) and S1 (a withheld extension's indexes kept on a folded base) are fixed in024ea96e5, the pin's type inf2ce2c0f7. S2 (the roll-up index reads the served fold) and S3 (another package's rule reading a withheld field) are filed by the seat, not fixed here.Acceptance notes
400 VALIDATION_FAILEDnaming the rule until the field's package is enabled again. This is the answer any rule reading an undeclared field already gets (fail closed). A suite whose modules read each other's fields should declare that dependency; whether a disable should then be refused, or such a rule skipped, is a ruling, not this card./meta/object. A page or form layout that names a withheld field explicitly is not measured here (ui lane paused).GET /meta/object/:name?layers=truemeasured while disabled:codeandeffectivecarry none of the module's fields (the code layer is the metadata-service copy folded byfoldObjectExtendersOnto). The registry's own code-layer lookup (resolveOwnerLayer→getArtifactItem, artifact identity for the save gates and the fallback when the service holds no copy) still folds every contribution: it answers "what does the code ship".🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6CbdPwGwxWkDNgrE4LE