Skip to content

os package publish silently substitutes a derived id when the artifact declares a manifest.id it will not use — an authorable key the publish path drops without a word #16891

Description

@claude

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:

  1. 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.
  2. 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).
  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions