Skip to content

feat(lint): warn when a package namespace is too long to have a paired customization namespace - #28775

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-sv8nh2
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-sv8nh2

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #28473

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:新增一条只告警、不拒收的作者期规则 namespace-no-customization-pair:包的 manifest.namespace 超过 13 个字符时,os validate / os build / os lint 提示"这个包没有配对的 _custom 定制命名空间",并给出 os explain 说明。
  • 为什么改:按 spec: the override declaration, the .custom pairing constants, the <base_ns>_custom namespace rule, the __c reservation, the overridable-type list and the base-field display whitelist #28404 的裁决(「同意」),命名空间超长的基础包拿不到配对定制包,确保定制包的入口会直接拒绝,安装方永远无法定制它;以前作者要等客户第一次定制被拒才知道,那时改命名空间等于改所有对象名和表名。
  • 风险与回滚:只多一条 warning,不拒收任何东西,也不进运行时发布门;仓库内已发布的包和示例最长命名空间是 13 字符,没有一个会触发。回滚即撤销本 PR。
  • 先看:packages/lint/src/validate-customization-namespace.ts(判定只读 spec 的 customizationNamespace,不另写数字)、packages/lint/src/authoring-rules.ts 末尾的注册项、packages/lint/src/rule-explanations.ts 的新条目。
  • 两分钟验证:pnpm --filter @objectstack/lint exec vitest run src/validate-customization-namespace.test.ts,预期 13 个用例全过(13 字符静默,14 字符告警,.custom 配对包本身静默)。
  • 需要你做的:无,席位落地。

What changes

  • New advisory rule validateCustomizationNamespace (packages/lint/src/validate-customization-namespace.ts), rule id namespace-no-customization-pair, severity warning. For each package one install delivers (packages[].manifest when the stack carries packages[], the top-level manifest otherwise; an artifact's own wrapper manifest is not judged), it asks customizationNamespace(manifest.namespace) from @objectstack/spec/kernel and reports exactly its base_namespace_too_long refusal, at manifest.namespace (or packages[N].manifest.namespace).
  • One reader. The verdict is the helper the ensure door creates the pair with. The sentence is built from MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH and CUSTOMIZATION_NAMESPACE_SUFFIX; the rule spells neither 13 nor _custom itself. The grammar maximum it prints is MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH + CUSTOMIZATION_NAMESPACE_SUFFIX.length, the identity customization-package.ts defines the limit by.
  • Not judged: a package that is itself a pair (customizationPackageId answers base_is_customization_package: a pair is never paired again); the helper's base_namespace_missing and base_namespace_invalid refusals (the second is os validate's grammar refusal).
  • Registry entry in AUTHORING_RULES: tier: 'advisory', input: 'normalized', all three commands, surfaces: CLI_ONLY with a surfaceReason (a package manifest is not a metadata item, so the runtime publish gate has nothing to judge; the ensure door already refuses at 409).
  • os explain namespace-no-customization-pair: a new entry in packages/lint/src/rule-explanations.ts (the table os explain reads; packages/cli is unchanged).
  • Barrel: validateCustomizationNamespace, NAMESPACE_NO_CUSTOMIZATION_PAIR and the CustomizationNamespaceFinding type from @objectstack/lint's root entry (the rule-id barrel test requires the id there).
  • Changeset: .changeset/28473-namespace-no-customization-pair.md, @objectstack/lint patch.
  • Docs, forced: four CLI transcripts print the author-time rule count, which the registry derives (authoringRulesFor), 58 → 59: content/docs/deployment/cli.mdx, content/docs/deployment/validating-metadata.mdx, content/docs/getting-started/build-with-claude-code.mdx, content/docs/ui/react-pages.mdx (check:docs-transcript-drift reds without it).

Why

ADR-0146 D3 names a base package's paired customization package's namespace BASE_NS_custom. The manifest namespace grammar admits 20 characters and the suffix takes 7, so a base namespace over 13 characters has no pair namespace. The ruling on #28404 (comment 6549129352, 「同意」) answers that with a named refusal and no truncation or hashing, and the ensure door refuses such a base with 409. Its installers can never customize it, and its author would learn that only when a customer first tried. This is follow-up (a) of that ruling.

Tests

  • packages/lint/src/validate-customization-namespace.test.ts (new, 13 cases): the fixtures sit on the helper's own boundary; 13 characters silent; 14 characters warns at manifest.namespace with severity warning; the message names warehouse_mgmt_custom; a .custom pair with a 20-character namespace is silent while the same namespace on a base warns; missing, empty and grammar-invalid namespaces are silent; no manifest.id still judged; artifact packages[] judged per body and the wrapper never; the CLI per-package view of each body yields the union run's findings byte for byte (so the CLI's per-package de-duplication adds no second copy); registry entry shape; all three commands report it; barrel and explain entry present.
  • packages/lint/src/rule-explanations.test.ts: one added case holds the explanation's written numbers and suffix (the module imports nothing) to the spec constants.
  • Forced test edits: none. Forced docs edits: the four transcript counts above.

Acceptance notes

  • The dispatch expected the os explain catalogue in packages/cli. It lives in packages/lint/src/rule-explanations.ts (published as @objectstack/lint/rule-explanations); the CLI's os explain and rule: pointer read it, so packages/cli needs no edit. packages/cli/test/explain-rule-id.test.ts iterates every entry through the command and covers the new one.
  • Public surface, measured: @objectstack/lint's root entry gains one function, one rule-id constant and one type, and RULE_EXPLANATIONS gains one key (its type is unchanged). No accept set changes: the rule never emits error. Other new lint rule ids landed under Clause-②: no (for example .changeset/26719-list-view-chart-foreign-dataset.md).
  • Boundary, not filed: the ensure door also refuses a base that declares no namespace (base_namespace_missing, 409). This rule does not judge that case; the ruling names the long one.

Verification

Final head 365377b2 (main merged at 5493b884). The lint tree is byte-identical between the merge commit 0780c708 and the final head (the last commit touches content/docs only), so the lint and CLI readings taken on 0780c708 hold for it.

Tests
  • pnpm --filter @objectstack/lint test on 0780c708: Test Files 188 passed (188), Tests 7777 passed (7777). pnpm --filter @objectstack/lint typecheck: exit 0.
  • pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated on 0780c708: exit 0 (spec moved on main's side; nothing to regenerate here).
  • pnpm --filter @objectstack/cli exec vitest run --project unit on 0780c708: 364/365 files, 5174/5175 tests. The one red, src/utils/override-declarations.test.ts, arrived with the merge (main's flow-override change) and read a stale metadata-protocol dist; after rebuilding the packages the merge moved (metadata-protocol, objectql, service-automation) it passed 7/7. Before the merge, on 15f94cb9: 363/365, the two reds being published-subpath-*.pin.test.ts needing the CLI's own dist; after pnpm --filter @objectstack/cli build, 29/29.
  • CLI explain and per-package seams: test/explain-rule-id.test.ts, test/union-fold-command-parity.test.ts, test/lint-per-package-authoring-seam.test.ts, test/validate-per-package-authoring-seam.test.ts, test/per-package-dedup-positional-echo.test.ts: 5 files, 36 tests passed.
  • eslint over the six changed packages/lint/src files: exit 0.
End to end, through the public door

A throwaway defineStack config with manifest.namespace: 'warehouse_mgmt' (14 characters), run with the built CLI:

  • os validate: exit 0, and prints the warning package "com.acme.warehouse": namespace "warehouse_mgmt" is 14 characters, so this package has no paired customization namespace: "warehouse_mgmt_custom" would be 21, past the 20 a namespace may have, and installers cannot customize the package, then fix: shorten manifest.namespace to 13 characters or fewer, and rename the object names it prefixes to match, then rule: namespace-no-customization-pair at manifest.namespace — os explain namespace-no-customization-pair for why a long namespace leaves a package uncustomizable.
  • os lint: exit 0, the same warning.
  • With warehouse_ops (13 characters): os validate prints the rule id 0 times.
  • os explain namespace-no-customization-pair prints the five paragraphs.
Ablation (rule source, read from src by the suite)

Through scripts/ablation-replace.mjs, each leg restored to the HEAD blob (51500056606b, git diff HEAD empty):

  • The verdict guard replaced so the rule never reports: validate-customization-namespace.test.ts 6 failed / 7 passed (the 14-character, message, pair-control, no-id, artifact and all-commands cases).
  • The pair-self skip removed: 1 failed / 12 passed (the .custom pair case).
Dispatch gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 365377b2 listed 88 commands (58 before the docs edit added content/docs paths). All 88 run, each with stdin from /dev/null and its exit captured before any pipe: 87 exit 0 on the first pass. pnpm check:dts-closure first read exit 1 (organizations' declaration files absent mid-run while another gate's build touched that package); after pnpm --filter @objectstack/organizations build it read exit 0 (241/241 declaration files present). Before running, 13 packages without a dist/ were built, so check:dual-build-cjs-loads measured instead of answering exit 3; check-plugin-teardown-shape --self-test read its pinned commit after git fetch --depth=1.

Reconciliation: dispatch-gates --ran: 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero).

CI check-runs on this PR: not read yet (in progress at the time of writing).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No schema touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (eslint on the six changed files: exit 0; the repo-wide run is CI's.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (The 维护者速读 section is Chinese by the repo's maintainer rule.)
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…d customization namespace

A new advisory authoring rule, `namespace-no-customization-pair`, reports a
delivered package whose `manifest.namespace` gets no `<ns>_custom` pair
namespace (ADR-0146 D3): `customizationNamespace` answers
`base_namespace_too_long`, so the ensure door will refuse to create the
`.custom` pair and installers can never customize the package. The verdict
and the sentence read the spec helper and its constants; nothing restates the
limit or the suffix. Runs on `os validate`, `os build` and `os lint`, CLI
surface only, with an `os explain` entry.

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

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 14 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/declarative-endpoints.mdx (via com.acme.crm (literal, a string literal in a comment in CustomizationNamespaceFinding))
  • content/docs/deployment/app-dependencies.mdx (via com.acme.crm (literal, a string literal in a comment in CustomizationNamespaceFinding))
  • content/docs/deployment/cli.mdx (via com.acme.crm (literal, a string literal in a comment in CustomizationNamespaceFinding))
  • content/docs/deployment/index.mdx (via com.acme.crm (literal, a string literal in a comment in CustomizationNamespaceFinding))
  • content/docs/deployment/install-modes.mdx (via com.acme.crm (literal, a string literal in a comment in CustomizationNamespaceFinding))
  • content/docs/deployment/publish-and-preview.mdx (via com.acme.crm (literal, a string literal in a comment in CustomizationNamespaceFinding))
  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object))
  • content/docs/kernel/contracts/metadata-service.mdx (via com.acme.crm (literal, a string literal in a comment in CustomizationNamespaceFinding))
  • content/docs/protocol/kernel/http-protocol.mdx (via com.acme.crm (literal, a string literal in a comment in CustomizationNamespaceFinding))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 9 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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 — 4 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 850eab2a948528d63d687dca8e77f26ebe7ed787 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 850eab2a948528d63d687dca8e77f26ebe7ed787

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

@objectstack-fleet

objectstack-fleet Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Head-sha: 365377b205331175cd86e3c4a81b6f945f5f7ced · read 2026-11-01T19:04Z

维护者速读

  • 改了什么:新增一条只告警的作者期规则 namespace-no-customization-pair。包的 manifest.namespace 超过 13 个字符时,os validate / os build / os lint 会提示这个包没有配对的 _custom 定制命名空间,安装方将无法定制它。
  • 为什么改:spec: the override declaration, the .custom pairing constants, the <base_ns>_custom namespace rule, the __c reservation, the overridable-type list and the base-field display whitelist #28404 的裁决(「同意」)规定超长命名空间拿不到配对定制包。以前作者要等客户第一次定制被拒才知道,那时再改命名空间要连带改所有对象名。
  • 风险与回滚:只多一条 warning,不拒收任何东西,也不进运行时发布门。仓库里最长的命名空间正好 13 个字符,没有一个包会触发。回滚就是撤销本 PR。
  • 先看:packages/lint/src/validate-customization-namespace.ts(判定直接调 spec 的 customizationNamespace,不另写数字)。
  • 两分钟验证:pnpm --filter @objectstack/lint exec vitest run src/validate-customization-namespace.test.ts。
  • 需要你做的:无。非受管面,席位在全绿后落地。

席位意见:可落地。实现只读 spec 自己的 helper 和常量,没有另抄一份限值;边界(13 静默、14 告警、.custom 配对包跳过)都有用例钉住。

席位核对 — ACCEPT · domain:spec#1 · head 365377b2

  1. Shape: draft, base main, first line Fixes #28473, no other closing keyword in the body. The claimed surface is packages/lint/src/** + .changeset/; the four content/docs/** transcript edits (58 → 59) are forced by check:docs-transcript-drift and were read line by line: each changes only that count. check-governed-merges --pr 28775: not governed, 385 changed lines, no human merge owed.
  2. Premise, measured: customizationNamespace, MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH, CUSTOMIZATION_NAMESPACE_SUFFIX and customizationPackageId are imported from @objectstack/spec/kernel; the rule reports base_namespace_too_long only and builds its sentence from the constants. The diff's rule module spells neither 13 nor _custom; the numbers in the explain text are held to the constants by rule-explanations.test.ts.
  3. Gate check-runs (pinned to head 365377b2): at reading 22 success, 8 skipped, 0 failed, the rest running (Test Core, Test UI, Type Check workspace/consumer/debt; Lint & Repo Gates queued). Landing needs every check green or an expected skip, with Lint & Repo Gates and the TypeScript Type Check jobs success. Dev's local readings: dispatch-gates 88/88 run, lint 7777/7777, CLI seams 36/36.
  4. Accepted deviations: ① the os explain table lives in packages/lint/src/rule-explanations.ts, not packages/cli, so cli is unchanged; packages/cli/test/explain-rule-id.test.ts walks every entry and covers the new one. ② Clause-②: no stands: the rule only warns, so no accept set changes; the root entry gains one function, one rule-id constant and one type, which the lint lane declares no (precedent: fix(lint)!: refuse an app nav visible that does not parse, and page variables that are duplicated, unnamed or wrong-typed #28723 and fix(formula,lint)!: a formula whose label() names a field that declares no options is refused at author time (#28659) #28714 added lint root exports under no). ③ The rule id namespace-no-customization-pair stays.
  5. Out of scope: base_namespace_missing is also refused at the ensure door and is not judged here; the ruling names only the long case, so it stays in Acceptance notes, not filed. The four-door table in validating-metadata.mdx is left as is. Skills-docs: n/a (Metadata-protocol: none).
  6. Landing: once every check on the current head is green (or an expected skip), the seat readies and queues it through the relay (pr_ready + automerge_enable). A head move voids this record until re-read.

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

1 participant