Filed by the domain:cli execution seat while implementing #16520 (session session_015QE8qk46e5CHJxyQEUjbf8), as a metadata-authoring trap that card does not cover. Filed unassigned and bare — no domain:*, no priority: that is triage's field, not this seat's. Routing looks like domain:cli; please confirm rather than inherit it.
What was measured
Read at origin/main c930f8597, in packages/cli/src/commands/package/publish.ts.
manifest.id is an authorable key. ManifestSchema.id (packages/spec/src/kernel/manifest.zod.ts) is a bare z.string() whose TSDoc reads Unique package identifier (reverse domain style) — a shape stated in prose and enforced by nothing. An author writing objectstack.config.ts therefore has no way to be told that the value they wrote is not usable.
os package publish reads that key through deriveManifestId, and when the value is not a manifest id the control plane accepts, it does not report anything: it falls through to local.SLUG-OF-MANIFEST-NAME, and publishes under that instead.
manifest.id = 'crm' in the artifact
manifest.name = 'Acme CRM'
os package publish prints: Registering package 'local.acme-crm'...
creates: sys_package.manifest_id = local.acme-crm
The declared value is discarded. Nothing in the output says the author's manifest.id was read, judged unusable and replaced — the substituted id merely appears in the ordinary progress line, indistinguishable from the case where the author declared nothing at all.
Why this is a trap and not an observation
PackageSchema.manifestId documents the column as immutable once set: "renaming a package requires creating a new package". So the value silently chosen here is a permanent, globally unique identifier the author never wrote and cannot rename afterwards. The author's own declaration is the one thing the CLI is sure not to use.
It is the shape Prime Directive #10's corollary names — a key declared with a meaning the runtime does not deliver — and it reaches an author, not a maintainer: the prose in the schema tells them to write reverse-domain, nothing refuses them when they do not, and the publish then quietly uses something else.
What #16520 does and does not change here
#16520 narrows the accept set of the publish preflight onto PackageSchema.manifestId, so the set of manifest.id values that take this silent-substitution path gets larger, not smaller: it now includes every shape the control plane refuses (an underscore, upper case, a digit-first segment, an empty segment, a trailing dot), where before it included only the shapes its local look-alike rule refused. That card's acceptance is about which values are accepted; it says nothing about whether the fallback should be silent, so this is deliberately left for a separate decision rather than widened into it.
The decision this needs
Not obviously one-way, which is why it is a card and not a patch:
- Say it out loud and continue — print a warning naming the declared value, why it was not usable (quoting the schema, as the refusal already does) and the id being used instead. Cheapest; keeps every current publish working.
- Refuse — treat a declared-but-unusable
manifest.id as an error, the way an unusable --manifest-id already is. Loudest, and consistent with "prefer failing to falling back", but it stops publishes that work today (an artifact carrying manifest.id = 'crm' publishes fine right now as local.crm-slug).
- Enforce at the producer — give
ManifestSchema.id the pattern its own TSDoc describes, so the value is refused at build/authoring time and the publish path never meets one. Contract-first, and the largest blast radius: it is a spec narrowing on an authorable key with existing values in the wild.
This seat has no ruling to lean on for the choice and has not made one.
Related: #16520.
Generated by Claude Code
Filed by the
domain:cliexecution seat while implementing #16520 (sessionsession_015QE8qk46e5CHJxyQEUjbf8), as a metadata-authoring trap that card does not cover. Filed unassigned and bare — nodomain:*, no priority: that is triage's field, not this seat's. Routing looks likedomain:cli; please confirm rather than inherit it.What was measured
Read at
origin/mainc930f8597, inpackages/cli/src/commands/package/publish.ts.manifest.idis an authorable key.ManifestSchema.id(packages/spec/src/kernel/manifest.zod.ts) is a barez.string()whose TSDoc readsUnique package identifier (reverse domain style)— a shape stated in prose and enforced by nothing. An author writingobjectstack.config.tstherefore has no way to be told that the value they wrote is not usable.os package publishreads that key throughderiveManifestId, and when the value is not a manifest id the control plane accepts, it does not report anything: it falls through tolocal.SLUG-OF-MANIFEST-NAME, and publishes under that instead.The declared value is discarded. Nothing in the output says the author's
manifest.idwas read, judged unusable and replaced — the substituted id merely appears in the ordinary progress line, indistinguishable from the case where the author declared nothing at all.Why this is a trap and not an observation
PackageSchema.manifestIddocuments the column as immutable once set: "renaming a package requires creating a new package". So the value silently chosen here is a permanent, globally unique identifier the author never wrote and cannot rename afterwards. The author's own declaration is the one thing the CLI is sure not to use.It is the shape Prime Directive #10's corollary names — a key declared with a meaning the runtime does not deliver — and it reaches an author, not a maintainer: the prose in the schema tells them to write reverse-domain, nothing refuses them when they do not, and the publish then quietly uses something else.
What #16520 does and does not change here
#16520 narrows the accept set of the publish preflight onto
PackageSchema.manifestId, so the set ofmanifest.idvalues that take this silent-substitution path gets larger, not smaller: it now includes every shape the control plane refuses (an underscore, upper case, a digit-first segment, an empty segment, a trailing dot), where before it included only the shapes its local look-alike rule refused. That card's acceptance is about which values are accepted; it says nothing about whether the fallback should be silent, so this is deliberately left for a separate decision rather than widened into it.The decision this needs
Not obviously one-way, which is why it is a card and not a patch:
manifest.idas an error, the way an unusable--manifest-idalready is. Loudest, and consistent with "prefer failing to falling back", but it stops publishes that work today (an artifact carryingmanifest.id = 'crm'publishes fine right now aslocal.crm-slug).ManifestSchema.idthe pattern its own TSDoc describes, so the value is refused at build/authoring time and the publish path never meets one. Contract-first, and the largest blast radius: it is a spec narrowing on an authorable key with existing values in the wild.This seat has no ruling to lean on for the choice and has not made one.
Related: #16520.
Generated by Claude Code