Skip to content

feat(spec): a package-id segment may open with a digit, and the refusal states the rule the pattern enforces - #20172

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-19870-manifest-id-digit-segment
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-19870-manifest-id-digit-segment

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #19870

Clause-②: yes

Implements ruling 5807946372 on this card (maintainer, verbatim: 「19870 去掉」; re-graded p2 in 5808347937), items 1 to 5, in the package-ID region of manifest.zod.ts / manifest.test.ts only. The VERSION-grammar hunks that draft PR #19637 carries in the same two files are untouched; a no-driver merge-tree probe of this head against origin/main 369bcbe is clean.

What changes

  • MANIFEST_ID_PATTERN (packages/spec/src/kernel/manifest.zod.ts) becomes /^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)+$/ (item 1). Additive widening: every id legal before stays legal; com.163.crm, com.example.2app and local.2024-app become legal. Still refused: a hyphen-led segment, an underscore, upper case, a single segment, an empty segment.
  • The refusal sentence (manifestIdRefusal) now reads ... Expected reverse-domain notation ('com.steedos.crm', 'org.apache.superset') — lowercase dot-separated segments of letters, digits and inner hyphens; a segment may not open with a hyphen; underscores are not admitted. The pattern docblock loses 「Each segment starts with a letter」 and says why a digit may open a segment; the id TSDoc states the rule (item 2). MANIFEST_ID_EXAMPLES unchanged. A digit-led bare word now gets the prefixed repair (2fa gets Did you mean 'com.example.2fa'?), where it used to get none.
  • Pins (item 3), manifest.test.ts: one ACCEPTED / REFUSED table. com.example.2app moved to accepted with com.163.crm beside it; each accepted row asserts the id parses to itself. com.example.-app, com.example.my_app and Com.Example.App stay refused, each asserting invalid_format and the declaration's own sentence. The PackageSchema.manifestId door is judged once on the same table, the expected verdict read from the table and never from the pattern. One more pin: the sentence names digits and the leading-hyphen rule and states no leading-letter clause.
  • manifest.namespace is untouched (item 4). Its leading-letter rule stays for the SQL-identifier reason.
  • Changeset: @objectstack/spec minor (item 5), .changeset/19870-manifest-id-digit-led-segment.md.

Repo-wide pin sweep (card-class clauses ① and ②)

Swept by RULE, not by literal: every string literal in git ls-files (ts/mts/mjs/js/json/md/mdx/yml, release-owned trees excluded) that the OLD pattern refuses and the NEW one accepts. That gave 586 raw hits, triaged by hand to the package-id ones. Every same-semantic pin is flipped in this PR, and each flipped pin asserts the new behaviour rather than just dropping the old assertion:

where was now
packages/metadata-protocol/src/protocol.install-manifest-id.test.ts com.4example.crm a REFUSED row an ADMITTED lit control: installs, and the registry receives the id as written. The refused reason is kept by com.-example.crm (hyphen-led) at the install and duplicate doors
packages/cloud-connection/src/marketplace-install-local-id-gate.test.ts com.1acme.crm a REFUSED row a lit control: 200, manifestId echoed, registered and filed as com.1acme.crm.json. The refused reason is kept by com.-acme.crm
packages/cli/test/package-publish-manifest-id.test.ts 9foo.bar in the relaxation table; 2024 App derived local.2024-app and was refused CLI and spec agree 9foo.bar, com.163.crm, local.2024-app are ids. 2024 App publishes: exit 0, both POSTs, wire manifest_id local.2024-app, not rewritten. Every slug the derivation can produce, from the name or the filename, parses
same file, anti-transcription control the old pattern literal, described as "the schema's own pattern" the widened literal

Prose that stated the retired rule, made false by this change:

  • packages/spec/src/migrations/entries/semantic/18.manifest-id-reverse-domain-required.ts: the unreleased v18 prescription said "each opening with a letter". Reworded, and registry.ts regenerated with gen:migration-registry (not hand-edited).
  • content/docs/deployment/cli.mdx and the publish.ts docblocks: they said local.2024-app is rejected, and listed 9foo.bar as a shape the control plane refuses. These are comment and docs changes only.

⚠ Three PENDING release notes corrected (confirmation requested)

check-empty-changeset is red on this PR by design. This is its DELIBERATE CORRECTION class. Three pending changesets on main quote the refusal sentence verbatim in a text block, and this PR changes that sentence. Each block now quotes what the door will actually print. Nothing else in those notes moved: their "If you are refused" prose was already true under the widened rule.

  • .changeset/19417-install-door-parses-manifest-id.md: the pkg-a refusal quote.
  • .changeset/19417-protocol-install-primitive-parses-manifest-id.md: the com.example.my_erp refusal quote.
  • .changeset/19576-install-local-id-gate.md: the late-app refusal quote.

Restoring them from base would ship a sentence no door prints. skip-changeset is deliberately not applied. Please confirm these three edits in writing on this PR. Check Changeset is not a required context.

PM mechanism hypotheses — measured

  • H1, census. None of the seven named files keeps its own copy of the rule. Each imports MANIFEST_ID_PATTERN, manifestIdRefusal or ManifestSchema, or names them in a comment. Pins that refused a digit-led segment under another spelling: the three consumer rows above. Two producers still assume a letter-led last segment and prefix a digit-led name (2fa becomes app-2fa): manifestIdSlug (packages/cli/src/commands/init.ts) and deriveManifestId (packages/create-objectstack/src/rewrite-identity.ts). Their output stays legal, so they are left as they are (Acceptance notes).
  • H2 holds, by reading the running code. deriveNamespaceFromPackageId from the built spec gives: com.example.2app → app, com.163.crm → crm, com.4example.crm → crm, com.example.123 → null. namespace-prefix.test.ts is green, and so is the metadata-protocol duplicate-door namespace test.
  • H3, objectui. It has one local copy of the rule: PACKAGE_ID_RE in packages/app-shell/src/views/studio-design/packages-io.ts:244, the same at the pin f8a9d0fb05 and at origin/main c3a26cc. That copy requires a letter first, lets a later segment open with a digit, and admits underscores. The one new drift this PR adds: a digit-led FIRST segment (9foo.bar) is now legal in spec, and the Studio duplicate form's client check still refuses it. objectui does not import the spec constant, and nothing is removed or renamed, so the Console Pin Gate is unaffected.
  • H4, cloud. NOT MEASURED: the repository is not attached to this container. The ruling's own reading (no DB-level pattern on manifest_id, cloud b524051) is carried as a lead.

Verification, at head a97a38db0

  • @objectstack/spec: full suite vitest run --project local: 540 files, 15800 passed, 2 todo. typecheck (tsc, scripts, test layer): exit 0. Focused (manifest, namespace-prefix, package-namespace, template-manifest-id, package-api): 5 files, 188 passed.
  • Consumer packages, one reading each:
    • metadata-protocol (protocol.install-manifest-id, protocol.duplicate-package-target-namespace): 32 passed, typecheck 0 errors.
    • runtime (packages-install-manifest-id, artifact-granted-permissions, domain-handler-registry): 101 passed.
    • cli (init-manifest-id, nav-contribution-groups.package-id, package-publish-manifest-id): 43 passed, typecheck 0 errors.
    • create-objectstack (rewrite-identity): 22 passed.
    • cloud-connection (marketplace-install-local-id-gate): 23 passed. It has no typecheck script.
  • Ablation, a one-time proof run from the committed state through scripts/ablation-replace.mjs. Both legs landed on disk, and each restore was proven: blob == HEAD blob f0913fa, git diff HEAD empty.
    • Leg A put the pattern back to the leading-letter rule: manifest.test.ts went 5 red. That is the two accepted digit rows, the shared-table door test, the 2fa repair, and the every-suggestion-parses test.
    • Leg B put the old sentence clause back: 1 red, the sentence pin.
    • Direction: turns red, as predicted.
  • Generated artifacts: pnpm --filter @objectstack/spec check:generated --fix reports all 15 up to date against a dist it built. The only regenerated file is registry.ts.
  • Gates: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran gives 115 derived, 110 run, 5 NOT MEASURED, 0 unrun. Of the 110 run, 109 exit 0 and check-empty-changeset exits 1 by design (above).
    • NOT MEASURED, prerequisite refusal (exit 3), needing a whole-workspace build: check:dual-build-cjs-loads, check:i18n, check:i18n-coverage, check:i18n-walk-parity.
    • NOT MEASURED, no verdict: check:type-check-debt. Its whole-workspace re-measure was cut off by the batch timeout.
    • CI owns all five.

Acceptance notes

None of these is filed. Each is an observation with no carrier (承接者:无).

  • The scaffolders' prefix for a digit-led project name (2fa becomes app-2fa in os init; 123 becomes com.example.app-123 in create-objectstack) is no longer required by the rule. Their comment "A segment must OPEN with a letter" now states a retired clause. Their output pins stay true as pins of the output.
  • os package publish: the refusal remedy arms for DERIVED sources (manifestIdRemedy, artifact-manifest-name / artifact-filename) are now unreachable. A slug is always one legal segment, so a derived id always parses. The arm for a declared manifest.id stays live.
  • objectui PACKAGE_ID_RE (H3): a second transcription of the rule. It was already looser (it admits underscores, which the server refuses loudly) and is now stricter on a digit-led first segment. Not exercised.
  • deriveNamespaceFromPackageId drops a leading digit run, even when that run is part of a word (com.example.2fa-leave gives fa_leave). This is pre-existing, only a default, and the author can declare namespace.
  • The Studio plugin id (studio/plugin.zod.ts, and content/docs/plugins/development.mdx) keeps its own leading-letter pattern. It is a different key under a different rule, not a package id.

Generated by Claude Code

…tates the rule

MANIFEST_ID_PATTERN widens to admit a digit-led segment (com.163.crm,
com.example.2app); a segment still may not open with a hyphen and
underscores stay refused. manifestIdRefusal and the id TSDoc now state
the rule the pattern enforces. Consumer pins that asserted a digit-led
segment is refused flip to accepted rows (metadata-protocol,
cloud-connection, cli publish), and texts quoting the old rule or the
old sentence follow (migration entry, cli docs, pending changesets).

Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Co-authored-by: Claude <noreply@anthropic.com>
…trol names the widened pattern

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/spec, touching 4 documentable anchor(s).

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

  • content/docs/api/declarative-endpoints.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/concepts/metadata-lifecycle.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/deployment/cli.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/deployment/index.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/deployment/publish-and-preview.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/getting-started/quick-reference.mdx (via ManifestSchema (symbol, a top-level const object))
  • content/docs/plugins/development.mdx (via ManifestSchema (symbol, a top-level const object))
  • content/docs/protocol/kernel/index.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/kernel/lifecycle.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/kernel/metadata-service.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/kernel/plugin-spec.mdx (via ManifestSchema (symbol, a top-level const object))
  • content/docs/protocol/objectql/index.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/objectql/schema.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))

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

  • content/docs/releases/v15.mdx (via ManifestSchema (symbol, a top-level const object))
  • content/docs/releases/v9.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))

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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 207 client-bound route-ledger rows — the other 153 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 153: 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; 98 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 — 142 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 369bcbeda7bb6c2cbc8c3c3892435351506c960c → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 369bcbeda7bb6c2cbc8c3c3892435351506c960c

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Check Changeset red on head a97a38db05143feadc46fe4d661b6db870cc2301 · domain:spec seat 2 (session_01QcAS3qiYYZNezaxZxaUdMV) · 2026-09-27T05:08Z

The failure is check-empty-changeset's DELIBERATE CORRECTION class. This PR edits three PENDING changesets (19417-install-door-parses-manifest-id, 19417-protocol-install-primitive-parses-manifest-id, 19576-install-local-id-gate), and only in their verbatim quotes of the refusal sentence this PR changes. Under landing-operations.md the confirmation is a same-head at-tier contract-review PASS record that names each corrected note and judges each rewritten sentence. That review is running now, and its record will be posted on this PR. ⛔ No skip-changeset, ⛔ no re-run, ⛔ no gate change. Check Changeset is not a required context, so the red rides into the queue under the SKILL.md three conditions once the record is in. If the record FAILs a rewrite, the PR goes back to the dev for a patch round.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 66/66 CONTRACT_REVIEW_TIER
Head-sha: a97a38db05143feadc46fe4d661b6db870cc2301

① Derived judgments

  1. Ruling items 1–5 vs diff.

    • Item 1: MANIFEST_ID_PATTERN is exactly /^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)+$/. An exhaustive probe over every string of length ≤7 on the alphabet ab19-._ finds 0 ids the old pattern accepted and the new refuses, so the widening is purely additive.
    • Item 2: the refusal sentence and the id TSDoc carry the ruling's wording verbatim; the docblock line 「Each segment starts with a letter」 is gone; MANIFEST_ID_EXAMPLES is unchanged.
    • Item 3: one ACCEPTED / REFUSED table. com.example.2app and com.163.crm are accepted and parse to themselves. com.example.-app, com.example.my_app and Com.Example.App are refused with invalid_format and the declaration's own sentence. PackageSchema.manifestId is judged on the same table.
    • Item 4: no hunk touches manifest.namespace or namespace-prefix.ts.
    • Item 5: @objectstack/spec: minor.
    • One divergence: the ruling wrote Clause-②: no, while the PR and changeset declare yes (widening). Under the canonical reader (scripts/pm/clause2-line.mjs) the value answers whether the card widens the accept set, and this diff does, so the PR's spelling is the correct one. Non-blocking.
  2. Accept set and consumers. Probed:

    • Refused: -.a, a.-, com.example.-app; 123 and 2app (single segment); com..app, .com.app, com.app.; underscores; upper case.
    • Accepted: 123.456, com.example.123, 9foo.bar, local.2024-app.
    • Both doors share the constant (package.zod.ts:172-174).
    • No consumer breaks. The runtime map keys by the string. deriveNamespaceFromPackageId gives com.163.crm → crm and com.example.123 → null (namespace left unset). slugify in publish.ts yields [a-z0-9][a-z0-9-]* or 'app', so every derived local. id parses. Both scaffolders still prefix app-.
    • Residual, carried from the ruling's own wording: the sentence says 「inner hyphens」 while the pattern admits a trailing hyphen. The sentence over-refuses, so no author following it is refused. Non-blocking.
  3. Prose truth at head. Every sentence checked is true: the refusal sentence, the TSDoc, migration entry 18 (unreleased) and the registry.ts regeneration, the publish.ts docblocks, and the new changeset. One sentence in content/docs/deployment/cli.mdx is imprecise: it lists three remedies where each refusable source prints a subset. Every listed remedy works, so no author is misdirected. Non-blocking.

  4. DELIBERATE CORRECTION. Check Changeset (both runs) exits 1, with one annotation per file under the foreign-changeset rule, naming exactly three notes:

    • .changeset/19417-install-door-parses-manifest-id.md
    • .changeset/19417-protocol-install-primitive-parses-manifest-id.md
    • .changeset/19576-install-local-id-gate.md

    In each note the single change is inside the quoted refusal block. It was … — lowercase dot-separated segments; hyphens allowed inside a segment, underscores are not. Did you mean '…'? and is now … — lowercase dot-separated segments of letters, digits and inner hyphens; a segment may not open with a hyphen; underscores are not admitted. Did you mean '…'? (for pkg-a → com.example.pkg-a, com.example.my_erp → com.example.my-erp, and late-app → com.example.late-app respectively).

    manifestIdRefusal was reconstructed from the head source: each quoted block, unwrapped, equals the head output byte for byte. Each rewrite is TRUE and stays inside the quote, and no other line of the three notes moved. Their 「If you are refused」 paraphrase states a subset of the widened rule and remains true. Confirmed as the deliberate-correction class; do not restore from base.

  5. Repo-wide pins. The old regex literal appears nowhere at head, generated reference docs included. No remaining test asserts a digit-led id as refused. Stale comments still state the retired clause (packages/cli/src/commands/init.ts:99, packages/create-objectstack/src/rewrite-identity.ts:113, rewrite-identity.test.ts:156); they are disclosed in the PR, and their output pins hold. Non-blocking.

② Semver level

minor is right. The 2026-09-04 ruling (batch #35, #15294) makes a purely additive widening of a published accept set at least minor. All packages sit in one fixed group, so metadata-protocol, cloud-connection and cli (tests plus a comment-only docblock) need no note of their own. content/docs releases nothing.

③ Boundary flags

  • Scaffolder app- prefix: out of scope. The output stays legal; the two comments state a retired rule.
  • os package publish derived-source remedy arms: verified unreachable. Dead but harmless.
  • objectui PACKAGE_ID_RE (packages-io.ts:244): out of scope for this card. It does not import the spec constant, and it is now stricter than the server on 9foo.bar. Nothing reaches the server wrongly. It deserves its own objectui card.
  • deriveNamespaceFromPackageId digit strip: pre-existing code, first reachable on a legal id through this widening. The ruling accepted it.
  • Studio plugin id: a different key with its own pattern, correctly untouched.

Implemented-by: claude/issue-19870-manifest-id-digit-segment
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 05:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 3bd28e2 Sep 27, 2026
41 of 44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19870-manifest-id-digit-segment branch September 27, 2026 05:44
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] manifestIdRefusal never says each segment must start with a letter, so com.example.2app satisfies every clause of its sentence and is refused

2 participants