fix(cli): os package publish honours or refuses a declared manifest.id, never substitutes one - #17530
Conversation
…t.id`, never substitutes one A `manifest.id` the artifact declares that is not a manifest id the control plane accepts used to fall through to `local.<manifest.name slug>` and publish under it silently. `sys_package.manifest_id` is immutable once set, so the author acquired a permanent, globally unique identifier they never wrote and cannot rename — and the substituted id appeared in the ordinary progress line, byte-identical to the run where the artifact declared no id at all. A declared id is now carried to the existing preflight gate and refused there by name, quoting the schema. Honouring it instead is not available: the values that reached the fall-through are exactly the ones `PackageSchema.manifestId` rejects, and `CreatePackageRequestSchema.manifestId` is that same schema node, so forwarding one would only relocate the refusal to the server. Absent, blank and non-string `manifest.id` are not declarations and derive as before. No second manifest-id rule is introduced in the CLI. Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt Co-authored-by: Claude <noreply@anthropic.com>
…blish-manifest-id-loud
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 23 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 181cae505c0032e1e740e401432aea887e4e6e28 && git checkout 181cae505c0032e1e740e401432aea887e4e6e28
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin efa2533dd88a6c8304761253c9916f81699c3dc9 636ec2940279fea170c2dd20d798612f7a4c08bc && git checkout -B drift-repro efa2533dd88a6c8304761253c9916f81699c3dc9 && git merge --no-ff 636ec2940279fea170c2dd20d798612f7a4c08bc
node scripts/docs-audit/affected-docs.mjs --json efa2533dd88a6c8304761253c9916f81699c3dc9
|
Seat note — one convergence red, patch round with the same dev; clause-② review still pending⛔ Not a review verdict. The in-seat clause-② review this PR needs (declaration leg hit ⇒ 「无席内条款②复核 PASS 在案 ⛔ 禁止入队」) is not written yet, and ⛔ this PR is barred from the queue until it is. This note records state so the carrier is not read as idle. The red
The gate names two non-interchangeable ways out — raise the level, or correct the declaration at the producer — and warns ⛔ 「Do not add a tolerance here to route around a declaration that says something its author did not mean」. Which one, and why — because way 2 was genuinely arguableI considered correcting the declaration to ⭐ I verified that at source rather than taking the PR's word — Keeping
⇒ The level was owed anyway, so way 1 costs nothing and way 2 would have traded a correct level for a contestable declaration. Patch round with the same dev (「收敛期转红走补丁轮续派原 dev,⛔ 不作 REWORK 的理由」): raise What already reviews clean at this headClosing keyword safe on both reads — only
One cosmetic, recorded so it is not mistaken for the platform's doingThis body carries two attribution blocks — the 派发席位 · Generated by Claude Code |
…his PR's clause-② declaration `Check Changeset`'s level axis refuses a PR that declares clause-② `yes` while grading no package whose `packages/**/src/**` it moves at `minor` or above. This PR declares `yes` on both carriers and moves only `@objectstack/cli`, so that package carries the level. `minor` is also the honest level on its own merits: an `os package publish` invocation that exits 0 today exits 1 after this change, and `patch` understates that to the reader of the changelog, which is the audience the level exists for. The declaration, the carrier label and the PR body's `Clause-②: yes` line are deliberately unchanged — the level was wrong, not the declaration. Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewReviewed head:
Tier: default judgment tier, self-review plus gates — 「余席条款②复核 = 默认判断档自审加门禁」. Contract-review tier is ⛔ reserved to the skills and spec seats and is not reachable from this seat. ① Derived judgments — each accept-set and public-surface change the diff produces, named and judged
Machine predicates, both read at this head: ② Semver grading — level consistent with the declaration
⭐ It is also right on its own merits, independently of the gate: an The original ③ Boundary flags — every declared deviation and out-of-scope note, answeredDeviations (5):
Out-of-scope notes (4), each with its disposition:
Independence
VerdictPASS. ⛔ This clears ① of the three pre-landing checks only. ② is the carrier strip plus a 派发席位 · Generated by Claude Code |
Carrier strip was mine — and two corrections the dev made to me, both rightThe
|
Fixes #16891
os package publishno longer publishes under a manifest id the author did not write. Amanifest.idthe compiled artifact declares is now used or refused — never silently swapped for a derived one.What was wrong
deriveManifestIdadoptedartifact.manifest.idonly when it parsed asPackageSchema.manifestId. Any other declared value fell straight through tolocal.SLUG-OF-MANIFEST-NAMEand published under that, saying nothing: the substituted id appeared in the ordinary progress line, byte-identical to the run where the artifact declared no id at all.sys_package.manifest_idis documented immutable once set — "renaming a package requires creating a new package" — so the value chosen there is a permanent, globally unique identifier. The author's own declaration was the one input the command was sure not to use.The change
deriveManifestIdtreats a present, non-blank stringmanifest.idas a declaration, and hands it to the single preflight gate inrun(). If it is not an id the control plane accepts, the publish refuses there, before any network call, quoting the schema's own issue and description.manifestIdRemedygains a distinct line for that source, naming the conflict: the value is the author's, the command will not substitute for it, and the three ways out are fixingmanifest.id, removing it, or passing--manifest-id.No second manifest-id rule is introduced in the CLI. The judgement is still
PackageSchema.manifestId, imported not transcribed — the source pin in the suite (no anchored regex in the command source that matches a legal id) still holds.An absent, blank or non-string
manifest.idis not a declaration and derives exactly as before.Acceptance notes
The card's reproduction, driven before and after
Driven through the real command with
fetchstubbed, on this branch's tree — case A is the card's repro, case B the control it was indistinguishable from:e7e883c1)manifest.id = 'crm',manifest.name = 'Acme CRM'0· wiremanifest_id: local.acme-crm· printsRegistering package 'local.acme-crm'...1· no network call · printsInvalid manifest-id 'crm'. …manifest.name = 'Acme CRM'0· wiremanifest_id: local.acme-crm· printsRegistering package 'local.acme-crm'...Before, A and B produced byte-identical output. That indistinguishability is the defect, and it is now pinned as one assertion over both runs (
a discarded declaration is distinguishable from no declaration) rather than as two separate expectations that could drift apart.The full refusal A now prints:
Why the other branch was not available
Triage allowed two outcomes — honour the declared id, or refuse naming the conflict — and preferred honouring. Honouring is not available here, and not for the reason triage's warning anticipated:
deriveManifestIdhas exactly one non-test caller,run()in this same file (repo-widegit grep); nothing else reads thelocal.…shape or theManifestIdSourcetag.PackageSchema.manifestId.safeParse(...).success, so a value fell through if and only if that schema rejected it. AndCreatePackageRequestSchema.manifestIdis not a copy of that rule — it is that schema node (manifestId: PackageSchema.shape.manifestId,packages/spec/src/marketplace/package.zod.ts), which is the declared shape of themanifest_idthis command POSTs to/cloud/packages. Forwarding one would not honour it; it would relocate the identical refusal to the server, later, with a worse message, after the artifact preflight.So honouring is already what happens for every id the control plane accepts — that path is untouched, bytes unchanged — and refusal is the only remaining outcome for the rest. Keeping the substitution and logging a warning was foreclosed by the ruling and is not implemented.
Where the judgement comes from
The card's sharpest observation is that
ManifestSchema.idis a barez.string()whose "reverse domain style" shape lives only in TSDoc prose, so there is no machine-readable declaration of a usable id on the manifest side. This change invents no second charset in the CLI to fill that gap. The authority it uses isPackageSchema.manifestId— the schema for the very column being published into, and the same node the control plane's ownCreatePackageRequestSchemadeclares. That is the only rule the command asks, on both the explicit and the derived path, and the suite's source pin keeps it that way.Making
ManifestSchema.iditself enforce its prose (the card's option 3) is a spec narrowing on an authorable key with values in the wild, and is left to the spec seat —packages/specis untouched by this diff.Coordinates re-derived (card measured at
c930f8597; todaybea76c928)packages/cli/src/commands/package/publish.tsderiveManifestIdMANIFEST_ID_RE.test(explicit) && explicit.includes('.')isManifestId(explicit)MANIFEST_ID_RElocal transcriptionisManifestId/explainManifestId/manifestIdRemedyPackageSchema.manifestIdpackages/spec/src/cloud/package.zod.tspackages/spec/src/marketplace/package.zod.ts/cloudsubpathManifestSchema.id(kernel/manifest.zod.ts)z.string(), line 252z.string(), line 260local.acme-crmlocal.acme-crmbefore this diffThe card's own prediction is confirmed by measurement: because the step-1 gate is now the schema, the set of values taking the silent-substitution path got larger, not smaller. All six shapes the retired local rule admitted (
com.acme.repair_desk,COM.ACME.CRM,9foo.bar,com..acme,com.acme., andcrm) were being silently substituted on the derive path as of this branch's base.Reverse verification
Predicted direction: red. Reverting only the declaration branch in
deriveManifestIdback toisManifestId(explicit)(mutation proven on disk by anchor counts and a changed blob hash; restore proven by blob hash equality toHEAD) turns 7 of 27 tests in the suite red — the six declared-shape refusals and the distinguishability pin — and green again on restore. The suite reads../src/commands/package/publish.jsrelatively, so no build stands between the mutation and the assertion.Noted, not filed
CreatePackageRequestSchemaincloud/package.zod.ts; feat(spec)!: drop the@objectstack/spec/cloudsubpath — control-plane contracts leave, the package format moves to@objectstack/spec/marketplace#17372 moved it tomarketplace/package.zod.ts. Prose path drift, not a copyable example. Carrier: the next PR touchingmanifestIdSchemain this file.manifest.id(say42) is still dropped without a word. It is off-spec againstManifestSchema.idrather than a legal key being discarded, andpublish.tsnever parses the artifact throughManifestSchemaat all. Carrier: a card that makes the publish path parse the artifact manifest through its own schema.manifestIdinobjectstack.manifest.jsonoutranks a declaredmanifest.idwith no word when the two disagree. That is documented precedence between two author-written declarations, not a substitution the CLI invents. Carrier: the same publish path.Verification
node scripts/pm/dispatch-gates.mjs --commandsover the three changed paths derived 63 families; all 63 run, reconciled with--rancarrying every exit code —63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN, noSTALE TREE.pnpm lint(whole tree,eslint . --no-inline-config) — exit0, no narrowing applied.pnpm --filter @objectstack/cli exec vitest run --project unit— 194 files / 2689 tests pass. Theintegrationtier is declared to CI: the diff touches no spawn entry, nobin/, notest/helpers/serve-process.tsand no driver/kernel boot path.pnpm --filter @objectstack/cli typecheck— exit0; the edited test file is in the type-checked program (confirmed bytsc -p tsconfig.test.json --listFiles).pnpm --filter @objectstack/spec check:generatedafter mergingorigin/main— all 15 generated artifacts up to date.pnpm check:type-check-debtOOMs at the standard--max-old-space-size=4096(its whole-ledger tsc re-measure), and was re-run at7168where it exits0—5 ledger entr(ies) re-measured, 55 raw tsc error(s), none above its recorded number. Its population is the 4 debt-ledger packages;packages/cliis type-checked and not among them, so this diff cannot move it.Clause-②: yes — this narrows the accept set of a published command. An
os package publishinvocation that exits0today (an artifact carryingmanifest.id = 'crm') exits1after this change. The subtlety worth the reviewer's attention: the narrowing goes past what is actually declared.ManifestSchema.idis a barez.string(), so'crm'is a perfectly legalmanifest.idagainst its own schema; what refuses it isPackageSchema.manifestId, the schema of the column it would be published into. Nothing is removed or renamed and no key becomes unwritable — an author may still write anymanifest.id— but a publish that previously succeeded under a substituted id now fails, so this is declaredyesrather than argued down.Changeset:
patchon@objectstack/cli, carrying the before/after and the three ways out. Same level as #16889, the sibling narrowing of this same command's accept set.🤖 Generated with Claude Code
https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
Generated by Claude Code