Repository navigation
fix(metadata-protocol)!: the protocol install primitive parses manifest.id, and the duplicate door parses its target id (#19417) - #19574
Conversation
…rimitive (#19417) `ObjectStackProtocolImplementation.installPackage` spread the request into `any` and handed it to `SchemaRegistry.installPackage` with a second `as any`, so an id `MANIFEST_ID_PATTERN` refuses installed and persisted while `defineStack()`, `os build`, `os validate` and the publish face all refused the same id. #19473 closed the HTTP door, which is one CALLER of this primitive; `duplicatePackage` is a second and an embedder is a third. The gate asks the declaration by reference (`ManifestSchema.shape.id`) and surfaces its own sentence (`manifestIdRefusal`) rather than rewording it, ahead of every write and every derivation. `duplicatePackage` parses its target id at the top of the method, because its manifest write sits inside a best-effort `catch {}` that would otherwise swallow the refusal and report success. Both namespace derivations on the duplicate path move from a raw `id.split('.').pop()` to the spec helper `deriveNamespaceFromPackageId`, the one `installPackage` already used: the target namespace is spliced into every copied object name, and the Studio's default `<sourceId>-copy` derived `leave-copy`, minting names the object declaration refuses. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 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 8b05264c5bddd8a7e866bd62cc115880b997b4df && git checkout 8b05264c5bddd8a7e866bd62cc115880b997b4df
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b3615f1a4cd7f3ff59ff0548530daa042627f732 3128642fd60833d130844364f1b5e6c872efafb5 && git checkout -B drift-repro b3615f1a4cd7f3ff59ff0548530daa042627f732 && git merge --no-ff 3128642fd60833d130844364f1b5e6c872efafb5
node scripts/docs-audit/affected-docs.mjs --json b3615f1a4cd7f3ff59ff0548530daa042627f732
|
…espace through the manifest namespace declaration (objectstack-ai#19829) Fixes objectstack-ai#19577 Clause-②: no (narrowing) ## What changed `ObjectStackProtocolImplementation.duplicatePackage` (`packages/metadata-protocol/src/protocol.ts`, located by symbol) resolved its target namespace as `request.targetNamespace ?? deriveNamespaceFromPackageId(request.targetPackageId)`, and the loud `400` fired only on `!targetNs`. So an explicit `targetNamespace` crossed no gate. That value is written as the copy's `manifest.namespace` and spliced into every copied object name as `${targetNs}_${short}`, which means `targetNamespace: 'my-ns'` minted `my-ns_ticket`, a name the object declaration refuses. Both branches now pass **one** parse, before the source scan and before the target package record is minted: - **The declaration, by reference.** `ManifestSchema.shape.namespace` (exported from `@objectstack/spec/kernel`, already imported by `protocol.ts` for the id gate). It is the `manifest.namespace` rule itself (`/^[a-z][a-z0-9_]{1,19}$/`, `packages/spec/src/kernel/manifest.zod.ts`), not a copied regex. The parse input is `targetNs ?? ''`, because the declaration is `.optional()` and would otherwise pass an absent value. - **Refused, not sanitised.** An explicit value the declaration refuses is refused. It is never rewritten the way the derivation sanitises an id. - **The declaration's sentence.** The refusal opens with the key and the value, then carries the declaration's own issue message verbatim. For example: `Invalid package namespace 'my-ns' on` + backtick `targetNamespace` backtick + `. Namespace must be 2-20 chars, lowercase alphanumeric + underscore. It becomes the copy's manifest.namespace and the prefix of every copied object name.` The derived branch keeps its `Cannot derive a package namespace from 'ID'. Pass targetNamespace explicitly.` opener. Its reworded rule clause is replaced by the same declaration sentence. Neither message opens with a bracketed tag. - **No new error code.** Both refusals throw `{ statusCode: 400 }` with no `code`, exactly as the id refusal above them does. An HTTP boundary (`resolveThrownHttpError`) therefore answers `400 VALIDATION_ERROR` on both branches, which is the status-derived code the derived branch already answered. - The comment that blessed the gap ("An explicitly declared `targetNamespace` still wins untouched …") is rewritten to state the new truth: the explicit value still wins over the default, but it is parsed. ## Dispatch assumptions, measured - **A1 (confirmed).** On base `c11852406` the construct is exactly as described. The ablation below shows the explicit branch resolving for every value the declaration refuses. - **A2 (refuted in part).** There is **no** exported namespace validator or regex that `deriveNamespaceFromPackageId` sanitises toward. It tests a module-private `NAMESPACE_RE` in `packages/spec/src/kernel/namespace-prefix.ts`, which is byte-identical to the manifest declaration's regex but not exported. The nearest published declaration is `ManifestSchema.shape.namespace`, and that is what this PR uses. The same shape was used for the id: `ManifestSchema.shape.id`. - **A3 (confirmed).** The comment is rewritten. - **A4 (confirmed).** No new code is minted. The explicit refusal reuses the derived branch's status-only shape, and the tests assert `declaredCode` is absent. - **Producer location.** The fix is at the producer. `POST /api/v1/packages/:id/duplicate` (`packages/runtime/src/domains/packages.ts`) forwards a string `targetNamespace` verbatim to this method and maps the throw with `errorFromThrown(e, 500)`. That route therefore answers `400 VALIDATION_ERROR` with no route change. ## Tests New file: `packages/metadata-protocol/src/protocol.duplicate-package-target-namespace.test.ts`. The existing `protocol.install-manifest-id.test.ts` is **untouched** and still green, derived-branch refusal included. - **Refusal, 8 cases.** Hyphen `my-ns`, uppercase, leading digit, leading underscore, 1 char, 21 chars, padded `' leave2 '`, and `''`. Each case asserts: - the envelope via `resolveThrownHttpError`: `status 400`, `code VALIDATION_ERROR`, `declaredCode` undefined; - that the message **starts with** its first sentence; - that the message contains the declaration's own issue message, read from `ManifestSchema.shape.namespace.safeParse` and not retyped; - that nothing was installed, scanned or saved. - **Lit control, 4 cases.** `leave2`, `leave_copy`, 20 chars and 2 chars. Each still duplicates, `manifest.namespace` equals the value, and the written object names are exactly `${ns}_ticket`. - **Derived branch, 1 case.** Same envelope, and the declaration's sentence follows the `targetNamespace` remedy. Runs (all at HEAD `5e843ab81`, through `scripts/pm/os-verify-lock.sh`): - `pnpm --filter '@objectstack/metadata-protocol^...' build`: VERDICT command-exit 0. - `pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/protocol.duplicate-package-target-namespace.test.ts src/protocol.install-manifest-id.test.ts src/protocol.bracketed-refusal-opener-absence.test.ts`: `Test Files 3 passed (3)`, `Tests 36 passed (36)`. The bracketed-opener pin is green. - `pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2` (whole package): `Test Files 188 passed | 3 skipped (191)`, `Tests 2671 passed | 19 skipped (2690)`, VERDICT command-exit 0. - `pnpm --filter @objectstack/metadata-protocol typecheck`: exit 0. `tsc --noEmit --listFiles` includes the new test file (1 hit). - Consumer check: `pnpm --filter @objectstack/metadata-protocol build && pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/protocol-package-lifecycle.test.ts` gave `10 passed`. That test is a downstream caller passing the conforming `targetNamespace: 'iojn2'` through the built dist. The runtime integration suite `package-duplicate-adopt-org-scope.integration.test.ts` also uses `iojn2` only; it is declared to CI and was not run here. **Ablation (one-shot, after commit `5e843ab81`).** Tool: `node scripts/ablation-replace.mjs`, which restores itself on EXIT/INT/TERM, wrapped in the verify lock. It replaced the anchor `if (targetNs == null || !declaredTargetNs.success) {` with `if (!targetNs) {`, which is the base guard, so the new parse is removed. - On disk: anchor x1 → x0, replacement x0 → x1, blob `74ee23265ed4` → `63700594eaa0`. The subject resolves via the relative `./protocol.js`, so it runs from `src/` and needed no rebuild. - Result: `Tests 7 failed | 6 passed (13)`. All 7 explicit refusal cases failed with `expected the call to be refused, but it resolved`. - The 6 that stayed green are what was predicted: `''` (the base `!targetNs` guard already caught it), the 4 lit controls, and the derived case. - Restore: blob after restore `74ee23265ed4` equals the HEAD blob, and `git diff HEAD` is empty. ## Gates `node scripts/pm/dispatch-gates.mjs --commands` was run on HEAD `5e843ab81`: 61 commands, each run with its exit captured before any pipe. `--ran` verdict: `✓ dispatch-gates --ran: 61 derived famil(ies) accounted for — 59 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3).` - `pnpm check:lean-entry-closure` first answered exit 3 (PREREQUISITE NOT MET: objectql dist absent). It was re-run after `pnpm exec turbo run build --filter=@objectstack/objectql --concurrency=2` and gave exit 0 (`2 published condition(s) measured from a real load`). The record carries the rerun. - NOT MEASURED: `pnpm check:dual-build-cjs-loads`. Reason: PREREQUISITE NOT MET, since it reads the built output of every package (68 lack dist here). A full `pnpm build` does not fit the foreground cap, and this diff changes no `package.json`, `exports` or build config. CI runs it on a fresh full build. - NOT MEASURED: `pnpm check:type-check-debt`. Reason: PREREQUISITE NOT MET, since it needs the whole packages build closure. The only package touched is `@objectstack/metadata-protocol`, whose own `typecheck` exits 0. ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-changeset-no-major.mjs --self-test :: exit 0 node scripts/check-ci-filter-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-adoption.mjs :: exit 0 node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-keyed-text-bounds.mjs :: exit 0 node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 node scripts/check-platform-object-tenancy-census.mjs :: exit 0 node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 node scripts/check-plugin-teardown-shape.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 node scripts/check-registry-log-declared.mjs :: exit 0 node scripts/check-registry-log-declared.mjs --self-test :: exit 0 node scripts/check-rest-log-spy-declared.mjs :: exit 0 node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 node scripts/check-system-context-census.mjs :: exit 0 node scripts/check-system-context-census.mjs --self-test :: exit 0 node scripts/check-undeclared-dep-imports.mjs :: exit 0 node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 node scripts/docs-audit/check-affected-docs.mjs :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 3 pnpm check:durability-log-level :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:filter-alias-parity :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:issue-citations :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:logger-receiver-detach :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:objectql-double-limit :: exit 0 pnpm check:objectui-changeset :: exit 0 pnpm check:org-identifier :: exit 0 pnpm check:page-declaration-shape :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:slot-lookup :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 3 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 ``` **Narrowed lint (measured).** `pnpm exec eslint --no-inline-config --format json` was run over the two changed TS files: exit 0, and the JSON reports 2 files, 0 errors, 0 warnings. - ① Population, from `eslint.config.mjs`: `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` plus the `packages/**/*.{ts,…}` blocks. The changeset `.md` is in no `files` glob, so these two files are the whole lintable part of the diff. - ② File count: 2, read from the JSON output. - ③ Invariance: the config never enables type-aware linting (no `parserOptions.project`, no typed rules; stated at `eslint.config.mjs` around line 327). The baselines it reads (`scripts/slot-lookup-baseline.json`, `scripts/query-options-erasure-baseline.json`) are untouched. So this diff cannot move any verdict on an untouched file. ## Changeset `.changeset/19577-duplicate-package-explicit-namespace.md`: `minor` for `@objectstack/metadata-protocol`, declaring `Clause-②: no (narrowing)`, a **BREAKING for callers** banner and an `adr-0087: not-required (no-migration-prescription)` disposition. It was re-graded from `patch` in a patch round on the seat's call, following the same door's precedent PR objectstack-ai#19574. The claim's `Clause-②` VALUE (`no`) is unchanged; the `(narrowing)` arm carries the direction the changeset gates read. ## Acceptance notes - **The declaration's sentence under-states its own regex.** `ManifestSchema.shape.namespace`'s message, `Namespace must be 2-20 chars, lowercase alphanumeric + underscore`, and its TSDoc rule (`2-20 characters, lowercase letters, digits, and underscores only`) both omit "starts with a lowercase letter". Measured: `safeParse('1leave')` and `safeParse('_leave')` refuse with that sentence, although both values satisfy every clause of it. This PR surfaces the sentence verbatim, as the id precedent requires, so the explicit-refusal message inherits the gap. The derived-branch message used to spell the leading-letter clause itself. That is a spec-side fix outside this card's file surface, and it is reported to the seat as a finding. - `packages/spec/src/kernel/namespace-prefix.ts` keeps a private `NAMESPACE_RE` that duplicates the manifest declaration's regex. It is a second declaration of one rule; no drift is observed today. Carrier: none. - Reserved namespaces (`sys`, `base`, `system`) are named in the `manifest.namespace` TSDoc but are not part of its regex, so `targetNamespace: 'sys'` passes this parse. This PR validates the charset declaration only. Whether a `sys` copy is refused downstream is NOT MEASURED. Carrier: none. - objectstack-ai#19576 (install-local door) is not addressed here. The objectstack-ai#19417 derived-seam work is untouched apart from its refusal sentence now coming from the declaration. --- _Generated by [Claude Code](https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19417
Clause-②: no (narrowing)
Fixes, notPart of, and the reason is measured rather than assumed: the landing record5754746826held this card open for exactly one thing — thepackages/metadata-protocolseam,protocol.tsbuildingdupManifestwithid: request.targetPackageIdand writing it throughinstallPackagewith noManifestSchemaparse. That seam is closed here. Every other ask on the card already landed with #19473 and is verified present on this branch's base: the HTTP door'sManifestSchema.shape.id.safeParsegate, the refusal pins, and the REVERSEDdomain-handler-registry.test.tspin ([#19417] POST /packages refuses 'pkg-a' — the id the pattern refuses (the REVERSED pin)). Nothing on the card is left standing, so merging it should close it.Diff measured between
b3615f1a4cd7f3ff59ff0548530daa042627f732(the merge base,origin/mainat branch creation) and3128642fd60833d130844364f1b5e6c872efafb5(head). 3 files, +465 / -3.⏳ The measurement the order asked for FIRST — and it is not what the seam note feared
The order's blocking question: does the platform itself mint package ids that
MANIFEST_ID_PATTERNrefuses, through this door? Enforcing here narrows a live door whose docblock says it serves packages that never take thedefineStackpath.Measured answer: no — no first-party code mints a refused id through this door. The instrument and its controls:
protocol.installPackage, whole repoobjectql/src/engine.ts,service-package/src/index.ts) — litpackages/runtime/src/domains/packages.ts(POST /packages)ManifestSchema.shape.id.safeParsegate #19473 landed sits above theprotocolSvc.installPackage(...)call in the same branch, unconditionally ⇒ only conforming ids reach the primitive from there!pkgId, theversionleg) are present in the same readduplicatePackage(same file)request.targetPackageId, caller-supplied: its only non-test caller isPOST /packages/:id/duplicate, which reads it from the request body and checks only that it is non-emptyduplicatePackagenon-test callers = 1, enumeratedos initstampscom.example.+manifestIdSlug(name), andmanifestIdSlugforces a letter-initial segment — conforming by construction. The shipped example manifests arecom.example.crm/.showcase/.tododefineStackoccurrence counts inexamples/are non-zero — lit0cf2d6644bdb96a9a6784ef801ee6a60a5306bd8, cloned to read it)-copyappended — conforming whenever the source is. Nothing auto-generates a refused idtargetPackageIdoccurrence count in objectui is non-zero, every hit openedpackages/studio/srcandpackages/setup/srcdo not exist in this tree, and apackages/*/srcpathspec is dead againstgit grep. Every reading above names a real path and carries a control that hit.⇒ the narrowing refuses caller input, never a value the platform produces. No grandfather clause is invented, and nothing is routed around.
The fix, and why it is on the primitive
ObjectStackProtocolImplementation.installPackagespread the request intoanyand handed it toSchemaRegistry.installPackagewith a secondas any.grep ManifestSchemaover its 22,686 lines returned exactly 2 hits, both comments — no parse anywhere in the file.⭐ #19473 is not this door, and it is also not unrelated — measured, because both readings matter. It landed in
packages/runtime/src/domains/packages.ts, and that HTTP door does route throughprotocol.installPackagewhen the protocol service resolves. So its gate protects that one caller and nothing else:duplicatePackageis a second caller and an embedder holding the protocol object is a third. Gating a door buys that door; this gate is on the method every caller passes.Three changes, all in
packages/metadata-protocol/src/protocol.ts:installPackageparses the rawmanifest.idthroughManifestSchema.shape.id— by reference, never a copy of the grammar — ahead of the spread, the version default and the namespace derivation. The refusal is the declaration's own sentence (manifestIdRefusal), surfaced rather than reworded, and the throw carriesstatusCode: 400so an HTTP boundary answers 400 rather than the 500 an unannotated throw earns (resolveThrownHttpError).statusCodeis the spelling this file already uses for its 404.duplicatePackageparses its target id at the top of the method. It has to be there, not only ininstallPackage: the manifest write below sits inside a deliberately best-effortcatch {}, so a refusal raised only there would be swallowed and the caller would readsuccess: trueon a package with no manifest row — a silent partial state, strictly worse than the status quo this card set out to close. The position also honours thePOST /packages/:id/duplicate对一个启动中的代码包返回success:false, copiedCount:0, failedCount:0(空 base)—— ADR-0070 D4「复制成可写 base」对代码包是否本就该拷贝其对象? #14451 rule already on this door: refuse before the mint, or the empty shell is left behind. The key named istargetPackageId, the path the caller actually wrote.id.split('.').pop()to the spec helperderiveNamespaceFromPackageIdthatinstallPackagealready used. This is not cosmetic: the target namespace is spliced into every copied object name asnamespace + '_' + short, and an object name is/^[a-z_][a-z0-9_]*$/(packages/spec/src/data/object.zod.ts). The Studio's own default duplicate id — the source id with-copyappended — therefore derivedleave-copyand mintedleave-copy_ticket, a name the object declaration refuses. The helper answersleave_copy. The source side is the same rule read backwards: the prefix those rows actually carry is the oneinstallPackagestamped, so matching them with the raw split found nothing and the copy landed under the SOURCE's names — the collision the re-namespacing exists to prevent. An explicittargetNamespacestill wins untouched; when neither an explicit nor a derivable namespace exists, the door refuses loudly namingtargetNamespaceas the remedy instead of renaming rows with an empty prefix.Both directions pinned
packages/metadata-protocol/src/protocol.install-manifest-id.test.ts, 18 cases, all passing:idat all, and a whitespace-padded conforming id. Each asserts the 400 tag, the message identical tomanifestIdRefusalitself (a pin that retyped the sentence would go green on a reworded fourth sentence for one rule), and that neither writer ran — not the in-memory registry, not the durablepublish.com.example.crm,com.example.my-erpandorg.apache.supersetstill install, registry called exactly once with the id intact.com.example.my_erpstill containscom.example.my-erp, the mechanical repair the declaration verifies before offering.registry.installPackage,engine.findandsaveMetaItemall uncalled), against a lit control where a conforming target duplicates rows and all.leave_copyand writesleave_copy_ticket; an explicittargetNamespacewins; an underivable one refuses naming the remedy.Reverse verification — direction predicted BEFORE running, both legs restored and proven
Run through
scripts/ablation-replace.mjs, which proves the mutation reached disk by anchor count and blob hash and proves the restore againstHEAD(no baregit checkout --). The subject resolves fromsrchere — the suite imports./protocol.jsinside its own package — sodistis not on this resolution path.installPackageid gatebe9dd23ad9c8→8981aea29d39, restored tobe9dd23ad9c8,git diff HEADemptysplit('.').pop()back fortargetNsbe9dd23ad9c8→613737784401, restored tobe9dd23ad9c8,git diff HEADemptyGates — 61 commands, harvested at this head
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat3128642fd6(the script derived the change set itself from the merge base; ⛔ not a hand-written path list). 58 green, 0 red, 3 NOT MEASURED — then one of the three was converted by building its prerequisite, leaving 59 green / 2 NOT MEASURED.Green, by name:
check-adr-0087-registration(+ self-test),check-changeset-no-major(+ self-test),check-ci-filter-parity,check-closing-keyword-parity(+ self-test),check-comment-mask-adoption(+ self-test),check-comment-mask-corpus,check-empty-changeset(+ self-test),check-keyed-text-bounds(+ self-test),check-platform-object-tenancy-census(+ self-test),check-plugin-teardown-shape(+ self-test),check-registry-log-declared(+ self-test),check-rest-log-spy-declared(+ self-test),check-system-context-census(+ self-test),check-undeclared-dep-imports(+ self-test),docs-audit/check-affected-docs,docs-audit/check-drift-comment,pm/release-rehearsal-clone --self-test,spec check:duration-unit-keys,check:changeset-gate-self-tests,check:cross-package-test-inputs,check:dispatcher-error-vocabulary,check:doc-authoring,check:driver-memory-census,check:dts-closure,check:durability-log-level,check:engine-double-contract,check:filter-alias-parity,check:gitlink-declared,check:issue-citations,check:lean-entry-closure,check:logger-receiver-detach,check:nul-bytes,check:objectql-double-limit,check:objectui-changeset,check:org-identifier,check:page-declaration-shape,check:pm-changeset-deadline-census,check:published-files,check:query-options-erasure,check:refd-timer-probe,check:slot-lookup,check:sourcemap-no-sources-content,check:test-source-alias,check:tier-file-adoption,check:type-check-coverage,check:watch-hint-literal,check:where-matcher.check:lean-entry-closurefirst exited 3 (PREREQUISITE NOT MET — it loads built entry points and@objectstack/objectqlhad nodist); afterturbo run build --filter=@objectstack/objectqlit measured green: 2 published conditions, 15 packages, admitted set held exactly.NOT MEASURED, and recorded as such — neither a pass nor a failure:
check:dual-build-cjs-loads— exit 3,PREREQUISITE NOT MET: it reads built output and 68 packages have nodist/. That needs a whole-repo build, which is CI'sBuild Core.check:type-check-debt— exit 3, same class:--re-measurerefuses to record a number against an unbuilt closure, since an unresolved import invents TS2307/TS7006 and erases the real debt.lint.ymlbuilds the closure before this step.Plus, beyond the harvest:
pnpm --filter '@objectstack/metadata-protocol^...' buildgreen; the package's full suite green (185 files, 2,645 tests, 0 failures — no existing pin moved);pnpm --filter @objectstack/metadata-protocol typecheckgreen, with--listFilesconfirming the new test file is in the program; the three@objectstack/objectqlsuites that drive a REAL protocol instance green (29 tests); and the repo-wide unionpnpm lint(eslint . --no-inline-config) green at3128642fd6— the union, so no narrowing had to be proven.Scope held, and what is deliberately left standing
idleg alone.InstallPackageRequestSchema/ManifestSchemaare still not parsed whole here; the residual classes the HTTP door's own docblock records are untouched and are each their own narrowing.packages/specdoes not move, and was never opened: the declaration was already right, and the order fenced it out.SchemaRegistry.installPackage/ObjectQL.registerAppdirectly and never pass this primitive. Versionless and namespace-less manifests still install; their defaults simply run behind the id gate instead of ahead of it.Acceptance notes — observed, ⛔ not fixed here
packages/app-shell/src/views/studio-design/packages-io.tsexportsPACKAGE_ID_RE = /^[a-z][a-z0-9_.-]*(\.[a-z0-9_-]+)+$/, which admits underscores and digit-initial segments;MANIFEST_ID_PATTERNis/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/and its own TSDoc says underscores are NOT admitted. Both the package-create dialog and the duplicate dialog validate against the looser copy, so the Studio acceptscom.example.my_erpand the server has refused it since fix(runtime): POST /api/v1/packages parses the manifest's id leg #19473 — a second declaration of one rule, in the sibling repo. Reported for a card of its own; ⛔ not touched from here.duplicatePackage's explicittargetNamespaceis still unvalidated. An explicitly passedmy-nsis spliced into object names asmy-ns_x, which the object declaration refuses. This change only aligned the DERIVED default, which is the seam the order named.reassignOrphanedMetadatareadstargetPackageIdwithout parsing it, the same positional read one method over. Left alone deliberately: it rebinds rows to an EXISTING package rather than minting one, so it is a different question about a different door.Generated by Claude Code