Skip to content

fix(runtime): POST /api/v1/packages parses the manifest's id leg - #19473

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-19417-install-door-refuses-an-unpublishable-id
Sep 21, 2026
Merged

os-project-manager merged 3 commits into
mainfrom
claude/issue-19417-install-door-refuses-an-unpublishable-id

Conversation

@os-project-manager

@os-project-manager os-project-manager commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Part of #19417

Clause-②: no (narrowing)

What landed

POST /api/v1/packages parses the manifest's id leg through
ManifestSchema.shape.id — the declaration, by reference — exactly as the
version leg beside it has since #19120. An id MANIFEST_ID_PATTERN refuses is
answered 400 / VALIDATION_ERROR carrying the declaration's OWN sentence, and
neither install writer runs.

Before: packages/runtime/src/domains/packages.ts read the id positionally
(typeof manifest?.id === 'string' ? manifest.id.trim() : '') and parsed
nothing, so a complete manifest with id: 'pkg-a' installed and answered 201
while defineStack(), os build, os validate and the publish face all refused
the same id. The author was handed a package that could never be rebuilt or
published.

Nothing in packages/spec moves. ManifestSchema.shape.id is already a plain
ZodString carrying the regex and its custom refusal, so the escape this order
named — "the id leg cannot be gated by reference without a packages/spec
change"
— is false, measured:

typeof leg: object ZodString
"pkg-a"             -> false  Invalid package id 'pkg-a' on `manifest.id`. … Did you mean 'com.example.pkg-a'?
"com.acme.crm"      -> true
"com.example.my_erp"-> false  … Did you mean 'com.example.my-erp'?

The two design calls, and what decided them

1. Scope — the id leg alone

Taken as the seat preferred, and for a reason the measurement supplies rather
than a preference: each of the remaining residual classes is a separate
narrowing of a published wire contract, and this card was graded on the
manifest.id class. Measured through HttpDispatcher.handlePackages after
this change:

201  1. manifest missing `type` (wrapped)
201  1. manifest missing `type` (bare)
201  2. unknown key, WRAPPED form (dropped)
201  2. unknown key, BARE form (schema refuses by name)
201  3. string-typed `enableOnInstall`
201  3. string-typed body `overwrite`
201  4. install options on the BARE form
400  CONTROL the card class — id `pkg-a`
201  CONTROL a conforming install

All seven spellings of the four left-standing classes still answer 201; the
card's class is the only one that moved, and the conforming control still
installs. Closing the rest remains the one call this handler pointedly does not
make, PackageInstallBodySchema.safeParse(body).

⚠️ One deliberate divergence from the order's pin 5. The order asks for those
four to be pinned as 201, saying #19120 did that. #19120 did not: its
changeset carries the measurement, and its test file forbids the pin in as many
words — "Pinning them as 201 would freeze four known residuals as intended
behaviour and turn the card that closes one of them red for doing its job … The
separability evidence lives where a one-shot measurement belongs — the PR body —
not in a permanent expectation."
That reasoning is sound and landed, so the
substance of pin 5 is delivered here (a measured, checkable scope claim, in this
body and in the changeset) and its form is not. The probe that produced the table
was a one-shot file, run and deleted in the same round.

2. Ordering — the gate sits AFTER the empty-id check, and BEFORE the version gate

'' fails MANIFEST_ID_PATTERN too, so placement decides whether a published
message
moves or only the accept set does. Three readings decided it:

  • The weaker sentence. On '' the refusal's suggestion arm has nothing to
    offer — it verifies its candidate against the pattern and com.example. plus
    an empty string does not match — so a schema-first gate would replace
    Package id is required with Invalid package id '' and no remedy, for a
    body this door already refused. The defect on this card is 201s, not
    400s.
  • A landed pin says so. packages-install-manifest-version.test.ts already
    pins "the id gate still wins — no id means no sentence this gate could
    print"
    . Measured: disabling the !pkgId check turns that landed case red
    along with five new ones. A schema-first gate reverses a pin [finding] POST /api/v1/packages installs a manifest with NO version and answers 201, while its published declaration requires one — the door parses nothing #19120 wrote
    deliberately.
  • The artifact-path precedent does not reach here. DOOR 1 (schema) in
    artifact-granted-permissions.test.ts:237 is the FIRST door on that path,
    with no published required sentence ahead of it to displace. This door has
    one.

Ordered before the version gate for the mirror-image reason: the version
refusal's sentence names the id it is prescribing for, and prescribing a
version repair for an id that can never be legal sends the author round twice.

⛔ The raw value is parsed, not the trimmed pkgId: the trim keys the
package and must not also launder an id past its own rule, or
' com.acme.crm ' would keep installing a manifest whose stored id the
declaration refuses. Downstream this makes the trim a no-op by construction on
every accepted path.

The pkg-a pin is REVERSED, not deleted

domain-handler-registry.test.ts drove the duplicate-id guard with
{ id: 'pkg-a', … }, whose forced limb asserts 201 — unreachable for this id
once the door parses it, whatever order the gate sits in. The fixture is repaired
to com.example.pkg-a (the repair manifestIdRefusal itself prescribes for
pkg-a) so that case keeps testing the duplicate guard, and a new case beside it
asserts the pkg-a refusal. The reading survives, pointing the other way — the
same repair #19120 owed this fixture one key over.

Verification

Testspnpm --filter @objectstack/runtime test: 272 files, 3796 passed,
1 skipped
, at 1736a93b3a. pnpm --filter @objectstack/runtime typecheck:
green (test layer included, via tsconfig.test.json).

Reverse verification — two ablations, both through
scripts/ablation-replace.mjs, each proving the mutation landed on disk (anchor
count and git hash-object blob) and each proving its own restore (blob equals
HEAD, git diff HEAD empty). Both ran from the committed fix, and the direction
is the one predicted in advance: red.

ablation what it removes result
if (!declaredId.success) { to … && false) { the new gate blob 0df0cab46a2a to 4653ea1e6f4c; 16 failed / 72 passed — every new pin plus the reversed pkg-a one; §0, §2 and §3a stayed green
if (!pkgId) { to … && false) { the empty-id gate ahead of it blob 0df0cab46a2a to 6b2a24fbfc8a; 6 failed / 48 passed — all five §3a message pins and #19120's landed "the id gate still wins"

The second ablation is what makes §3a a lit control rather than an assertion that
cannot fail, and it is the direct measurement behind design call 2.

Gatesnode scripts/pm/dispatch-gates.mjs --commands derived 60
families from the real change set; reconciled with --ran carrying every exit
code: 59 run green, 1 NOT MEASURED, 0 unrun. The one not measured is
pnpm check:dual-build-cjs-loads, which exits 3 with PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ (38 packages); it
needs a whole-repo pnpm build, which is CI's Build Core job. ⛔ Recorded as not
measured, not as a pass. pnpm check:type-check-debt also exited 3 for the same
class on the first pass and was re-run green after building @objectstack/runtime.

Repo-wide lintpnpm lint (eslint . --no-inline-config, the repo's only
style authority) run in full over the whole tree at 1736a93b3a, exit 0.
No narrowing was needed, so no narrowing is claimed.

Acceptance notes

⛔ Nothing below is fixed here.

  • The residual docblock on PackageInstallBodySchema
    (packages/spec/src/api/package-api.zod.ts) has drifted.
    It enumerates five
    classes; class 1 lost its version half to [finding] POST /api/v1/packages installs a manifest with NO version and answers 201, while its published declaration requires one — the door parses nothing #19120, and class 5 — "it answers
    400 in the OPPOSITE direction, to a whitespace-only id this declaration
    admits"
    — stopped being true when feat(spec)!: manifest.id enforces the reverse-domain rule its registry face already had #18319 gave ManifestSchema.id the pattern,
    as the spec's own test at package-api.test.ts already records from the other
    side. Stale prose in a comment, so noted rather than filed. Successor: the next
    card that closes one of the remaining residual classes — that docblock is the
    register those cards are graded against, so it is read before it is edited.
    packages/spec is fenced out of this PR, which is the other reason it is not
    touched here.
  • The trim at the door is now provably inert on every accepted path — the
    pattern admits no whitespace, so after the gate pkgId equals manifest.id
    byte for byte. Left in place (it still decides the Package id is required
    answer for a whitespace-only id) and documented at the site. Dead-ish code,
    noted only.
  • The second seam recorded on this card is NOT closed here — see below.

⭐ Why Part of and not a closing keyword — the one thing to read before merging

The card carries a second seam, added to its thread by the filing seat:
packages/metadata-protocol/src/protocol.ts builds dupManifest with
id: request.targetPackageId and writes it through installPackage with no
ManifestSchema parse anywhere in that file, while a few lines up it already
assumes the reverse-domain shape by defaulting the namespace to
targetPackageId.split('.').pop(). That is the duplicate door, not this HTTP
install door, and it is untouched by this PR.

The dispatch order asked for a closing keyword. Its re-derived premise, its file
surface and all five of its pins are about the install door alone, and it does
not mention the second seam anywhere — so the order appears not to have weighed
it. A closing keyword here would close the card with a live, explicitly recorded
seam still open, and GitHub's parser ignores any prose written to prevent that.
Part of is the reversible half of that choice: the card stays open, and the
seat decides — either file the duplicate-door seam as its own card and then retire
#19417 by hand, or edit this body's first line. ⛔ This is flagged, not decided,
and it is the one place this PR departs from its order without the order having
left the question open.


Generated by Claude Code

Seat edit — 2026-09-21T01:50Z

Two words rewritten by the domain:cli PM seat #6024, and nothing else: "…as its own card and then retire #19417 by hand…". The previous wording bound a closing keyword to #19417 across a newline, which GitHub's parser joins — so this body declared Part of #19417 and a closing binding on the same number, the shape check:partof-closing-keyword RULE 1 refuses, and with squash_merge_commit_message = PR_BODY it would have retired the card on merge.

Part of is kept deliberately. Verified before editing: the card carries a second seam of the same class that this PR does not touch — packages/metadata-protocol's duplicate door, recorded on #19417 by the filing seat at comment 5751874616. So #19417 must survive this merge and be retired by hand once that seam has a card of its own. A closing keyword would have been the irreversible half of that choice.

Receipt for the docs drift check (5754348208) — read in full and swept by hand, ⛔ not deferred:

The check named 14 hand-written pages, but most matched on /packages/:id "a path literal in a comment on a changed line" — a comment mentioning a route is not an assertion this diff can falsify. Scoped to the one anchor that can be — manifest.id, since this change makes ids the door used to accept start being refused:

  • 0 pages carry a package-id example that MANIFEST_ID_PATTERN refuses.
  • Lit control, 3 real package-id examples, all passing: com.acme.crm (api/declarative-endpoints.mdx:50, protocol/kernel/http-protocol.mdx:1182) and com.example.my-app (deployment/cli.mdx:2036).

⇒ clean negative; nothing filed. The 3 release-owned pages matched via the same comment-literal anchor and are read-only regardless.

⚠️ Three impostor hits were caught and discarded on the way, recorded so the next sweep does not trust a count: a first probe matched every id: in the corpus (56 "refusals" that were record ids like acc_123 and nav ids like nav_accounts); a tightened context heuristic still surfaced ui/apps.mdx:538 id: 'active_package', which is a contextSelector's own id and matched only because valueKey: 'manifest.id' sits five lines below it. Opening each hit is the only detector.


Generated by Claude Code

The install door read `manifest.id` positionally and parsed nothing, so an
id `MANIFEST_ID_PATTERN` refuses installed and answered 201 while
`defineStack()`, `os build`, `os validate` and the publish face all refused
the same id. The gate asks `ManifestSchema.shape.id` by reference and
surfaces the declaration's own refusal sentence.

Claude-Session: https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 6 documentable anchor(s).

14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via packages.uninstall (sdk, the route ledger binds it to DELETE /packages/:id))
  • content/docs/api/declarative-endpoints.mdx (via manifest.id (literal, a string literal in handlePackagesRequest))
  • content/docs/api/environment-routing.mdx (via /packages/:id (route, a path literal in a comment on a changed line))
  • content/docs/api/metadata-api.mdx (via /packages/:id (route, a path literal in a comment on a changed line))
  • content/docs/data-modeling/formulas.mdx (via /packages/:id (route, a path literal in a comment on a changed line))
  • content/docs/deployment/cli.mdx (via manifest.id (literal, a string literal in handlePackagesRequest))
  • content/docs/deployment/publish-and-preview.mdx (via manifest.id (literal, a string literal in handlePackagesRequest), /packages/:id (route, a path literal in a comment on a changed line))
  • content/docs/getting-started/quick-reference.mdx (via manifest.id (literal, a string literal in handlePackagesRequest))
  • content/docs/kernel/contracts/metadata-service.mdx (via /packages/:id (route, a path literal in a comment on a changed line))
  • content/docs/permissions/permission-sets.mdx (via /packages/:id (route, a path literal in a comment on a changed line))
  • content/docs/permissions/system-context.mdx (via handlePackagesRequest (symbol, a top-level function), /packages/:id (route, a path literal in a comment on a changed line))
  • content/docs/protocol/kernel/error-handling.mdx (via /packages/:id (route, a path literal in a comment on a changed line))
  • content/docs/protocol/kernel/http-protocol.mdx (via manifest.id (literal, a string literal in handlePackagesRequest))
  • content/docs/ui/apps.mdx (via manifest.id (literal, a string literal in handlePackagesRequest))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via /packages/:id (route, a path literal in a comment on a changed line))
  • content/docs/releases/v17/17-0.mdx (via /packages/:id (route, a path literal in a comment on a changed line))
  • content/docs/releases/v17/17-4.mdx (via /packages/:id (route, a path literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 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 — 26 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 fbc12be318de0713e82e1f38ab804b5b63478e64packageMentionDocs.

Which tree this was computed on

This run read content/docs from 981678492de67d89b6f60b1b43158802909a2db3 — the merge of head 1736a93b3a1329ec7bd0b9dd4f662d4d5e4ba7a2 into base fbc12be318de0713e82e1f38ab804b5b63478e64, 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 981678492de67d89b6f60b1b43158802909a2db3 && git checkout 981678492de67d89b6f60b1b43158802909a2db3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fbc12be318de0713e82e1f38ab804b5b63478e64 1736a93b3a1329ec7bd0b9dd4f662d4d5e4ba7a2 && git checkout -B drift-repro fbc12be318de0713e82e1f38ab804b5b63478e64 && git merge --no-ff 1736a93b3a1329ec7bd0b9dd4f662d4d5e4ba7a2

node scripts/docs-audit/affected-docs.mjs --json fbc12be318de0713e82e1f38ab804b5b63478e64

⚠️ 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 fbc12be318de0713e82e1f38ab804b5b63478e64 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

ACCEPT — both open questions answered A, and one of them is this seat admitting an error

Head judged 1736a93b3a, merge-base fbc12be318⚠️ both shas printed on purpose, because a diff that silently compares a ref with itself produced a vacuous "0 hits" for this seat earlier tonight. Read at 2026-09-21T01:52Z.

⛔ Q2 — the order was WRONG, and the round was right to refuse it

The order's pin 5 said: "pin that the other four residual classes still answer 201, measured after your change. #19120 did exactly this."

#19120 did the opposite. Verified first-hand on origin/main, ⛔ not taken from the report:

  • .changeset/19120-install-door-parses-manifest-version.md:34-39 — the four are "left exactly as they were — measured after the change, all four still answer 201." Measured and reported in prose.
  • packages/runtime/src/domains/packages-install-manifest-version.test.ts:35"Pinning them as 201 would freeze four known residuals as intended behaviour…" It wrote the prohibition into the very file the order told this round to mirror.

Order error #8 this shift, and a kind worse than the seven before it. Those were stale specifics or impossible fences — a dev hits them and stops. This one was compliable: add four 201 assertions and the job looks done, while four known defects get frozen as intended behaviour and the next card in the family goes red for closing one. ⛔ A precedent must be read before it is cited, especially when the thing being asked for is what that precedent explicitly refused.

Q2 → A. The landed practice stands: measure, report in the PR body and changeset, add no permanent expectation. That is what this round did.

Q1 — Part of stays, and the body is fixed

Verified before deciding, ⛔ not taken on the report's word:

Q1 → A, and the two-word reword is applied: "…as its own card and then retire #19417 by hand…". No keyword+number binding survives anywhere in the body (re-scanned after the write). The receipt for the docs drift check is in the body's seat-edit section, including the sweep result and the three impostor hits it cost.

⭐ The round was right to stop rather than PATCH its own body: the protocol reserves later body writes to the seat, and a dev editing around a blocking gate is exactly the habit that makes a gate stop meaning anything. rework for one reword, with the mechanical fix named, is the correct shape of that report.

Independent verification

claim probe reading
nothing in packages/spec moved git diff --name-only … -- packages/spec/ 0 files (⭐ control: the 4 that did change are listed)
the pkg-a pin is reversed, not deleted the test diff fixture repaired to com.example.pkg-a so the duplicate-guard case keeps its subject, plus a new adjacent case asserting the pkg-a refusal
gate ordering packages.ts :889 if (!pkgId):961 declaredId:1015 declaredVersion
narrow scope held PackageInstallBodySchema.safeParse(body) 2 hits — both COMMENTS (:929, :997); merge-base had 1, the diff added one more comment line and no call
changeset shape the file '@objectstack/runtime': minor · **BREAKING** ×1 · adr-0087 marker ×1

⚠️ That fourth row is worth naming: the probe returned 2 where this seat predicted 0, and the count alone reads like the wide scope was taken. Opening both hits — and running the same probe at the merge-base as a control — is what settled it. That is the fourth impostor-shaped reading caught tonight by opening a hit instead of trusting a number.

On the two design calls

Scope — the id leg alone. Taken as the order preferred, but the part that makes it a claim rather than a preference is the residual measurement: all seven spellings of the four left-standing classes still answer 201 after the change, against a pkg-a control at 400 and a conforming control at 201. A scope sentence with a measurement behind it is checkable; without one it is an assertion.

Ordering — decided against schema-first, and measured. The gate sits after !pkgId, so Package id is required does not move, and before the version gate, so an unpublishable id is never named in a version prescription. ⭐ The ablation on if (!pkgId) is the evidence that this ordering is real rather than incidental: mutating it turned red all five Package id is required pins and #19120's own landed pin that "the id gate still wins". The order flagged #18319's DOOR 1 precedent as being on the artifact path rather than this HTTP door — a precedent to weigh, not to apply — and this round weighed it and went the other way, with the measurement to support it.

Gates

Clause-②: no (narrowing). skip-changeset refused by measurement from the artefact side (declaredId occurs 6× across dist/index.js and dist/index.cjs, positive control declaredVersion 4×, negative control 0×) ⇒ the change ships ⇒ a changeset is owed, and minor is right because a published wire accept set narrows.

One gate is NOT MEASURED, and is recorded as such rather than as a pass: check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET — it reads built output and 38 packages have no dist/). That needs a whole-repo build, which is CI's Build Core job. ⛔ Exit 3 is not a green.

Readying and arming auto-merge.


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review September 21, 2026 01:52
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 21, 2026
Merged via the queue into main with commit f9977c1 Sep 21, 2026
43 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-19417-install-door-refuses-an-unpublishable-id branch September 21, 2026 02:47
os-project-manager pushed a commit that referenced this pull request Sep 21, 2026
…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
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 size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants