Skip to content

[finding] ManifestSchema.id is a bare z.string() whose reverse-domain shape lives only in TSDoc, while its sibling PackageSchema.manifestId enforces that shape with a regex — one identifier, two declarations, only one of them machine-readable #17534

Description

@os-justin

Filed by the domain:cli execution PM seat (#6024), session session_01DapQyvYrFb1MxSYe7BL2nt, landing point measured as packages/spec ⇒ routed domain:spec. Grading, final routing and type are triage's. ⛔ Not claimed, ⛔ not dispatched, and ⛔ not a decision this seat is pre-empting — see "Why it may be not_planned".

Carried forward from #16891 so it does not close with it. #16891 named this as its "option 3" and is closing completed via PR #17530 on a different option; the only written statement of this problem would go with it. That is the whole reason this card exists.

The measurement

Two sibling declarations of the same identifier, at origin/main:

packages/spec/src/kernel/manifest.zod.ts:253-260 — the authorable side, where a human or an agent writes the value:

  /**
   * Unique package identifier using reverse domain notation.
   * Must be unique across the entire ecosystem.
   *
   * @example "com.steedos.crm"
   * @example "org.apache.superset"
   */
  id: z.string().describe('Unique package identifier (reverse domain style)'),

packages/spec/src/marketplace/package.zod.ts:167-170 — the column side, where the value lands:

  manifestId: z
    .string()
    .regex(/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/)
    .describe('Globally unique reverse-domain package identifier (e.g. com.acme.crm)'),

⇒ 「reverse domain notation」, 「Must be unique across the entire ecosystem」 and both @examples are prose and nothing else on the side an author actually writes. The rule exists, machine-readable, thirty lines away in another file — and :284 (manifestId: PackageSchema.shape.manifestId) makes that node the control plane's own request schema too.

Why it matters, with a driven consequence rather than a hypothetical

An author writes manifest.id = 'crm'. It parses clean against ManifestSchematsc green, os build green, no warning anywhere. It is refused only much later, by a different schema, at publish time.

⭐ That gap is exactly the defect #16891 was filed for, and PR #17530 measured the before/after through the real command: before, os package publish silently substituted local.acme-crm and exited 0, byte-identical to a run where the artifact declared no id at all. #17530 fixes the publish path to refuse instead of substitute — the right fix for that command — but it ⛔ cannot make ManifestSchema.id tell the author anything at authoring time, because the shape is not in the schema to enforce.

⚠️ This is the AI-author failure mode #4001 named repo-wide: 「人看到控件不对会去翻;AI 拿到一个成功响应,然后报告「已完成」」. A prose-only shape produces a clean parse, and a clean parse is a success response.

Why it may legitimately be not_planned — stated so the card does not read as advocacy

⚠️ This is a spec narrowing on an authorable key with values already in the wild. ManifestSchema.id has always accepted any string, so any existing artifact carrying a non-conforming id parses today and would stop parsing. That is a materially different cost from #17530's, which only changed one command's behaviour at publish time.

⛔ This seat did ⛔ not measure the installed-base blast radius and is ⛔ not recommending the change. Whoever takes this owes that measurement first — #4001's own method is the precedent: 「每一步都要能实证零破坏,而不是靠推断」, and #3746's cost evidence (three example apps validating clean, zero docs teaching a rejected spelling) is the shape of proof that made its sibling narrowing safe.

Two things a fix would have to decide, both non-obvious

  1. Which rule? Adopting PackageSchema.manifestId's regex makes the two nodes agree by construction — but it also means the authoring surface inherits the marketplace column's constraint, and a package that is never published now cannot use an id the publish path would have refused anyway. Whether that is correct or over-reach is the question, ⛔ not a detail.
  2. Refusal with a remedy, ⛔ not just louder. 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 is explicit: 「strict 必须配可修的错误信息,不能只是「大声」…要点名那个键,并在能识别时给出正确拼法」. The parts are already in the tree (shared/suggestions.zod.ts, data/object.zod.ts's UNKNOWN_KEY_GUIDANCE / suggestKey, shared/error-map.zod.ts, with ui/action.zod.ts as the worked template) and ⛔ must be reused rather than rebuilt.

Refs

#16891 (where this was option 3) · PR #17530 (the publish-path fix that closes it, and its 「Where the judgement comes from」 section) · #16889 (the sibling narrowing of the same command) · #4001 / #3746 (the repo's ruling and worked template for exactly this class) · #17372 (moved PackageSchema from spec/src/cloud/ to spec/src/marketplace/; coordinates above are post-move).

派发席位 · session_01DapQyvYrFb1MxSYe7BL2nt · R72 · 2026-09-10T20:30Z · 本评论来自 domain:cli 派发座位

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions