Repository navigation
feat(runtime): PATCH /api/v1/packages/:id edits a package's manifest dependencies - #24703
Conversation
…d before the fix) Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
… row The door judges the row by ManifestSchema's dependencies row and refuses a malformed one 400 before any write; SchemaRegistry.updatePackageManifest replaces the row whole and the protocol's updatePackage persists it. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…r the dependencies edit Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
📓 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:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 39 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 b8441ec96c9d3a46ce7793c9960aeddac0ead28f && git checkout b8441ec96c9d3a46ce7793c9960aeddac0ead28f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9b2953d62c50a851604afbe91e3a48c90d72645f 7a8557cff1d62fac022377bcdf91cb7950d1ba5f && git checkout -B drift-repro 9b2953d62c50a851604afbe91e3a48c90d72645f && git merge --no-ff 7a8557cff1d62fac022377bcdf91cb7950d1ba5f
node scripts/docs-audit/affected-docs.mjs --json 9b2953d62c50a851604afbe91e3a48c90d72645f
|
Contract reviewServed-tier: The face is the ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT
Accepted deviations:
Out-of-scope dispositions (one line each):
Landing: through the merge queue by this seat once every check on this head reads Generated by Claude Code |
|
Landing hold ·
Generated by Claude Code |
Resolves the PATCH /packages/:id arm conflict in packages/runtime/src/domains/packages.ts: main's ADR-0070 read-only gate (requireWritablePackage ... 'edit') is kept exactly as main has it, and this branch's wrapped/src/patch/dependencies handling follows it unchanged. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: This head is a merge of ① Derived judgments
② Semver levelUnchanged: ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · addendum: ACCEPT carried to
|
Fixes #24670
Clause-②: yes (widening)
What changed
PATCH /api/v1/packages/:idnow accepts the manifest'sdependenciesrow besidename,descriptionandversion, flat or under themanifestwrapper.packages/runtime/src/domains/packages.ts, thePATCH /packages/:idarm only). The row is judged byManifestSchema.shape.dependenciesfrom@objectstack/spec, the rowPOST /packagesparses at create. Nothing is restated. A malformed row is refused400 VALIDATION_ERROR, the envelope the create door answers for the same row, before the protocol or the registry is reached. So nothing is written, including a valid key sent beside it. The "nothing to update" 400 now namesdependenciesamong the accepted keys.packages/objectql/src/registry.ts,SchemaRegistry.updatePackageManifestonly). It copiesdependencieswhen present. The row replaces the stored one whole, never a deep merge, and the registry keeps its own copy. An entry the row leaves out is removed, and{}clears the declaration. Replacing the object, rather than editing it in place, also keeps the protocol's shallow pre-patch snapshot able to put the prior row back when the durable write fails.packages/metadata-protocol/src/protocol.ts,updatePackageonly). Thepatchtype gainsdependencies. Persistence needed no change:updatePackagealready re-persists the whole merged manifest throughpersistPackageManifest.The objectui half (Studio's edit dialog sends only the three keys) is not in this PR. Triage
6279169921routes it to objectui once this door accepts the row.Mechanism assumptions, measured
dependenciesintosys_packages. Holds on both branches. On the protocol branch, pin (a) reads thesys_packagesrow through the realPackageServicePluginand re-reads it after a restart. On the in-memory fallback, the door test reads the registry andGET /packages/:id.manifest.dependencieslive. Holds. The one runtime reader isresolveWritePackageScope(protocol.ts). It walksregistry.getPackage(id).manifest.dependencieson every write and caches nothing.git grepfound no other runtime reader ofmanifest.dependenciesinmetadata-protocol,objectql,runtime,services/*orlint. Pin (d) shows the next save sees the edit with nothing retired.POST /packageswithdependencies: { "com.acme.core": 1 }answers400 VALIDATION_ERROR. The PATCH answers the samestatusandcode, and a parity test pins this.dependencies.KEYormanifest.dependencies.KEY, following the form the caller wrote.Removing a dependency: readings for the seat
The PATCH applies the row as given, and no in-use check was built. Measured over the integration harness below, which runs the real
ObjectQL,SqlDriver(better-sqlite3),PackageServicePlugin, protocol on the default authoring channel andHttpDispatcher. Package P declares D. A list view stored in P binds to D's object. ThenPATCH /packages/Psends{ "dependencies": {} }, which answers200, and the steps below were measured with a one-off probe that is not committed:protocol.getMetaItem_diagnostics: { valid: true }GET /meta/view/NAME(dispatcher)200, no_diagnosticson the itemprotocol.saveMetaItem422 INVALID_METADATA, rulelist-view-object-outside-closure; the stored row is unchangedPUT /meta/view/NAME?package=P(dispatcher)422 INVALID_METADATA, rulelist-view-object-outside-closureloadMetaFromDb(){ loaded: 2, errors: 0, invalid: 0, storeUnavailable: false }; the view stays registereddependencies: {}servedloadMetaFromDb(){ loaded: 2, errors: 0, invalid: 0, storeUnavailable: false }; the view is registeredGET /meta/view/NAME200422 INVALID_METADATA,list-view-object-outside-closureSo an item stored before the removal keeps loading and serving, and neither the boot count nor the read badge flags it. Its next save is refused with the closure verdict, which names the remedy (declare the dependency again).
Tests
All runs are on head
0fcd8e42cunless noted.@objectstack/objectqland@objectstack/metadata-protocolresolve todist/in the runtime suites, so both were rebuilt before every reading.7a83ce21f, pre-fix build): 13 of 16 runtime cases and both registry cases failed. The(d)refusal step already passed (422 INVALID_METADATA,list-view-object-outside-closure) and the case failed at the PATCH:400 "Body { name?, description?, version? } — nothing to update". A validnamesent beside a malformed row answered200and was written.packages/runtime/src/domains/packages-patch-dependencies.test.ts: realHttpDispatcherover a realSchemaRegistry. Covers the row persisted flat and wrapped, whole-row replace, four malformed rows refused with nothing written, refusal before the protocol is called, parity withPOST /packages, and (c) the three existing keys as before.packages/runtime/src/package-dependencies-patch.integration.test.ts: (a) persisted, served, stored insys_packagesand served after a restart; (b) malformed refused with neither store moving, and asys_packages-refused edit (a trigger) answers500with the prior row back in the registry,GETand the store; (d) the remedy end to end.packages/objectql/src/registry.test.ts: two[#24670]cases.Tests 17 passed (17)andTests 103 passed (103).@objectstack/objectql(--project local):Test Files 448 passed (448),Tests 8449 passed (8449).@objectstack/metadata-protocol:Test Files 249 passed | 3 skipped (252),Tests 20781 passed | 19 skipped (20800).@objectstack/runtime(--project local):Test Files 427 passed (427),Tests 6695 passed | 19 skipped (6714).VERDICT command-exit 0.pnpm --filteron the three packages exits 0, including each package'scheck:test-typecheck.c88492672). Throughscripts/ablation-replace.mjs, the lineif (patch.dependencies !== undefined) manifest.dependencies = { ...patch.dependencies };was deleted: anchor 1 to 0, blob0d0948aec118tod2dd6f3e9d62.objectqlwas rebuilt, andablation-dist-preflight.mjs @objectstack/objectql "manifest.dependencies = {" --absentfound the marker absent from all 14 built files. Before the mutation it was present in 4 (core.js,core.mjs,index.js,index.mjs).0d0948aec118,git diff HEADempty. After a rebuild, the preflight found the marker in 4 files, the tree was clean, and the 17 + 2 cases were green.Gates
node scripts/pm/dispatch-gates.mjs --commandsat0fcd8e42cderived 72 commands.--ranreconciliation: 72 accounted, 71 exit 0, 1 NOT MEASURED.pnpm check:dual-build-cjs-loadsexits 3 with PREREQUISITE NOT MET. It reads the whole workspace'sdist/, and only this card's closure was built. CI'sLint & Repo Gatesruns it.check-engine-split-ratio --days 90andcheck-plugin-teardown-shape --self-testrefused on the shallow clone. After deepening (git fetch --shallow-since=2026-07-14), both exit 0.check-changeset-fixed,check:authz-resolver,check:error-code-casing,check:route-ledger-census) exit 0.pnpm lint(eslint . --no-inline-config) exits 0 at0fcd8e42c.Export measurement (sets the bumps)
The
dist.d.tswas compared before and after.@objectstack/objectql:index.d.tsandcore.d.tsare identical, apart from the chunk hash. One signature changed:SchemaRegistry.updatePackageManifest'spatchgainsdependencies?: Record(string, string).@objectstack/metadata-protocol: one signature changed:updatePackage'srequest.patchgains the same optional key.No export was added, removed or renamed. Both are accept-set widenings, so the changeset takes
minoron all three packages (runtime, objectql, metadata-protocol).Acceptance notes
EngineSchemaRegistryView.updatePackageManifest(packages/spec/src/contracts/objectql-engine.ts:131) types the patch asname,descriptionandversion. The door's in-memory fallback calls the registry through that contract type, and it compiles because the patch is a declared variable, not a fresh literal.packages/specis held off in this dispatch. Carrier: thedomain:specseat.main(packages:PATCH /api/v1/packages/:idedits a read-only package's manifest (200), where disable, publish, revert, delete and install-overwrite refuse it422 WRITABLE_PACKAGE_REQUIRED#24705, PR fix(runtime)!: PATCH /packages/:id refuses a read-only package with 422 WRITABLE_PACKAGE_REQUIRED #24719 →9b2953d62) and runs before this PR's body read. So a package the deployment ships answers422 WRITABLE_PACKAGE_REQUIREDfor adependenciesedit too, with nothing written. fix(runtime)!: PATCH /packages/:id refuses a read-only package with 422 WRITABLE_PACKAGE_REQUIRED #24719's pins cover a{ dependencies: {} }body on all four read-only kinds, on both limbs, and stay green on this branch after it mergedmain(merge7a8557cff). (This note replaces the round-1 note that said the arm asked no read-only question. Edited by thedomain:cliseat 2 after the merge.)updatePackageis typedanyin this file.PackagesDomainProtocol.updatePackageis still(request: any), in the "undeclared request shapes" group ofpackages.ts. Typing it from the producer's own signature, asrevertStoredPackageis, would make a key this door sends that the producer does not declare a compile error. Carrier: none.Generated by Claude Code